@sanctuary-framework/mcp-server 1.2.11 → 1.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -17618,11 +17618,426 @@ var init_handoff_log = __esm({
17618
17618
  }
17619
17619
  };
17620
17620
  COORDINATION_VIEW_AUDIT_OPS = {
17621
+ /** v1.3 Omega-1: operator opened the chronological handoff list. */
17621
17622
  VIEW_OPENED: "operator_coordination_view_opened",
17622
- ENTRY_DRILLED: "operator_handoff_entry_drilled"
17623
+ /** v1.3 Omega-1: operator drilled into a single handoff for detail. */
17624
+ ENTRY_DRILLED: "operator_handoff_entry_drilled",
17625
+ /**
17626
+ * v1.3 Omega-3: operator opened the Workflows sibling-view (list of
17627
+ * multi-handoff workflows grouped by `workflow-grouper`). Mirrors
17628
+ * VIEW_OPENED's shape so the dashboard activity feed can group both
17629
+ * as "operator coordination surfaces."
17630
+ */
17631
+ WORKFLOW_VIEW_OPENED: "operator_workflow_view_opened",
17632
+ /**
17633
+ * v1.3 Omega-3: operator drilled into a single workflow for its
17634
+ * timeline + member-handoffs detail. Mirrors ENTRY_DRILLED's shape.
17635
+ */
17636
+ WORKFLOW_DRILLED: "operator_workflow_drilled",
17637
+ /**
17638
+ * v1.3 Omega-3: server-side state transition observed on a
17639
+ * workflow (e.g., in_progress -> stalled). Emitted by the route
17640
+ * layer after the state tracker diffs against its prior snapshot.
17641
+ * Distinct from the operator-action events above: this records what
17642
+ * the workflow itself is doing, not what the operator clicked.
17643
+ */
17644
+ WORKFLOW_STATE_CHANGED: "coordination_workflow_state_changed"
17645
+ };
17646
+ }
17647
+ });
17648
+
17649
+ // src/coordination/context-transfer-extractor.ts
17650
+ async function extractContextTransferBreakdown(detail, deps = {}) {
17651
+ const pathA = tryStructuredPath(detail);
17652
+ if (pathA) return pathA;
17653
+ const pathB = tryCompositionPath(detail);
17654
+ if (pathB) return pathB;
17655
+ const pathC = tryHeuristicPath(detail);
17656
+ if (pathC.confidence >= 0.5 || !deps.substrateSelector) {
17657
+ return pathC;
17658
+ }
17659
+ const assist = await tryLlmAssistPath(detail, deps.substrateSelector);
17660
+ return assist ?? pathC;
17661
+ }
17662
+ function tryStructuredPath(detail) {
17663
+ const details = sourceDetails(detail.source_audit_entry);
17664
+ if (!details) return null;
17665
+ const transferredRaw = details["transferred"];
17666
+ const withheldRaw = details["withheld"];
17667
+ if (transferredRaw === void 0 && withheldRaw === void 0) return null;
17668
+ const transferred = parseExplicitContextItems(transferredRaw);
17669
+ const withheld = parseExplicitContextItems(withheldRaw);
17670
+ return {
17671
+ handoff_entry_id: detail.entry.entry_id,
17672
+ transferred,
17673
+ withheld,
17674
+ source: "structured",
17675
+ confidence: 1
17676
+ };
17677
+ }
17678
+ function tryCompositionPath(detail) {
17679
+ const op = detail.source_audit_entry.operation;
17680
+ if (!op.startsWith("composition_completed")) return null;
17681
+ const details = sourceDetails(detail.source_audit_entry);
17682
+ if (!details) return null;
17683
+ const receiptRaw = details["receipt"];
17684
+ const sourceStateRaw = details["source_state_snapshot"];
17685
+ if (receiptRaw === void 0) return null;
17686
+ const transferred = parseExplicitContextItems(receiptRaw);
17687
+ const withheld = [];
17688
+ if (Array.isArray(sourceStateRaw)) {
17689
+ const transferredKeys = new Set(
17690
+ transferred.map((t) => `${t.category}:${t.summary}`)
17691
+ );
17692
+ for (const item of parseExplicitContextItems(sourceStateRaw)) {
17693
+ const key = `${item.category}:${item.summary}`;
17694
+ if (!transferredKeys.has(key)) withheld.push(item);
17695
+ }
17696
+ }
17697
+ return {
17698
+ handoff_entry_id: detail.entry.entry_id,
17699
+ transferred,
17700
+ withheld,
17701
+ source: "composition",
17702
+ confidence: 0.9
17703
+ };
17704
+ }
17705
+ function tryHeuristicPath(detail) {
17706
+ const entry = detail.entry;
17707
+ const audit = detail.source_audit_entry;
17708
+ const details = sourceDetails(audit);
17709
+ if (audit.operation === "cross_harness_approval_aggregated") {
17710
+ const ruleId = optString2(details, "policy_rule_id");
17711
+ if (ruleId) {
17712
+ const category = categoryFromPolicyRuleId(ruleId);
17713
+ const summary = `${ruleId} (${entry.source_agent_id} -> operator)`;
17714
+ return {
17715
+ handoff_entry_id: entry.entry_id,
17716
+ transferred: [
17717
+ {
17718
+ category,
17719
+ summary: truncate(summary, SUMMARY_MAX_CHARS),
17720
+ size_hint: "minimal"
17721
+ }
17722
+ ],
17723
+ withheld: [],
17724
+ source: "heuristic",
17725
+ confidence: 0.5
17726
+ };
17727
+ }
17728
+ }
17729
+ if (audit.operation === "v1.1_local_handoff") {
17730
+ const reasonClass = optString2(details, "reason_class");
17731
+ const newStatus = optString2(details, "new_status");
17732
+ const previousStatus = optString2(details, "previous_status");
17733
+ const transferred = [];
17734
+ const withheld = [];
17735
+ if (newStatus === "denied" || newStatus === "failed") {
17736
+ withheld.push({
17737
+ category: "other",
17738
+ summary: truncate(
17739
+ `handoff ${newStatus}${reasonClass ? ` (${reasonClass})` : ""}: ${entry.source_agent_id} -> ${entry.target_agent_id}`,
17740
+ SUMMARY_MAX_CHARS
17741
+ ),
17742
+ size_hint: "minimal"
17743
+ });
17744
+ } else if (newStatus === "accepted" || newStatus === "completed") {
17745
+ transferred.push({
17746
+ category: "other",
17747
+ summary: truncate(
17748
+ `handoff ${newStatus}${previousStatus ? ` (from ${previousStatus})` : ""}: ${entry.source_agent_id} -> ${entry.target_agent_id}`,
17749
+ SUMMARY_MAX_CHARS
17750
+ ),
17751
+ size_hint: "small"
17752
+ });
17753
+ } else {
17754
+ transferred.push({
17755
+ category: "other",
17756
+ summary: truncate(
17757
+ `handoff ${entry.source_agent_id} -> ${entry.target_agent_id}${newStatus ? ` (${newStatus})` : ""}`,
17758
+ SUMMARY_MAX_CHARS
17759
+ ),
17760
+ size_hint: "minimal"
17761
+ });
17762
+ }
17763
+ return {
17764
+ handoff_entry_id: entry.entry_id,
17765
+ transferred,
17766
+ withheld,
17767
+ source: "heuristic",
17768
+ confidence: reasonClass || newStatus ? 0.5 : 0.3
17769
+ };
17770
+ }
17771
+ return {
17772
+ handoff_entry_id: entry.entry_id,
17773
+ transferred: [
17774
+ {
17775
+ category: "other",
17776
+ summary: truncate(
17777
+ `handoff ${entry.source_agent_id} -> ${entry.target_agent_id}`,
17778
+ SUMMARY_MAX_CHARS
17779
+ ),
17780
+ size_hint: "minimal"
17781
+ }
17782
+ ],
17783
+ withheld: [],
17784
+ source: "heuristic",
17785
+ confidence: 0.3
17786
+ };
17787
+ }
17788
+ async function tryLlmAssistPath(detail, selector) {
17789
+ const entry = detail.entry;
17790
+ const audit = detail.source_audit_entry;
17791
+ const probe = `event=${audit.operation} sender=${entry.source_agent_id} target=${entry.target_agent_id} summary=${entry.context_transfer_summary}`;
17792
+ try {
17793
+ const response = await selector.invokeClassify("sentinel-scoring", {
17794
+ kind: "classify",
17795
+ items: [probe],
17796
+ categories: [...CATEGORY_VALUES]
17797
+ });
17798
+ if (response.body.kind !== "classify") return null;
17799
+ const top = response.body.results[0];
17800
+ if (!top || !isCategory(top.category) || top.confidence < 0.4) {
17801
+ return null;
17802
+ }
17803
+ return {
17804
+ handoff_entry_id: entry.entry_id,
17805
+ transferred: [
17806
+ {
17807
+ category: top.category,
17808
+ summary: truncate(
17809
+ `LLM-classified handoff ${entry.source_agent_id} -> ${entry.target_agent_id}: ${top.category} (confidence ${top.confidence.toFixed(2)})`,
17810
+ SUMMARY_MAX_CHARS
17811
+ ),
17812
+ size_hint: "minimal"
17813
+ }
17814
+ ],
17815
+ withheld: [],
17816
+ source: "llm-assist",
17817
+ confidence: 0.6
17818
+ };
17819
+ } catch {
17820
+ return null;
17821
+ }
17822
+ }
17823
+ function sourceDetails(audit) {
17824
+ return audit.details;
17825
+ }
17826
+ function optString2(details, key) {
17827
+ if (!details) return null;
17828
+ const value = details[key];
17829
+ if (typeof value !== "string" || value.length === 0) return null;
17830
+ return value;
17831
+ }
17832
+ function isCategory(value) {
17833
+ return CATEGORY_VALUES.includes(value);
17834
+ }
17835
+ function truncate(s, cap) {
17836
+ return s.length <= cap ? s : `${s.slice(0, cap - 3)}...`;
17837
+ }
17838
+ function parseExplicitContextItems(raw) {
17839
+ if (raw === null || raw === void 0) return [];
17840
+ if (Array.isArray(raw)) {
17841
+ const out = [];
17842
+ for (const entry of raw) {
17843
+ if (typeof entry === "string") {
17844
+ out.push({
17845
+ category: "other",
17846
+ summary: truncate(entry, SUMMARY_MAX_CHARS),
17847
+ size_hint: "minimal"
17848
+ });
17849
+ continue;
17850
+ }
17851
+ if (entry && typeof entry === "object") {
17852
+ const obj = entry;
17853
+ const category = isCategoryValue(obj["category"]) ? obj["category"] : "other";
17854
+ const summary = typeof obj["summary"] === "string" ? truncate(obj["summary"], SUMMARY_MAX_CHARS) : "(unspecified)";
17855
+ const sizeHint = isSizeHintValue(obj["size_hint"]) ? obj["size_hint"] : "minimal";
17856
+ out.push({ category, summary, size_hint: sizeHint });
17857
+ }
17858
+ }
17859
+ return out;
17860
+ }
17861
+ if (typeof raw === "object" && raw !== null) {
17862
+ const out = [];
17863
+ for (const [k, v] of Object.entries(raw)) {
17864
+ const category = isCategoryValue(k) ? k : "other";
17865
+ if (Array.isArray(v)) {
17866
+ for (const item of v) {
17867
+ if (typeof item === "string") {
17868
+ out.push({
17869
+ category,
17870
+ summary: truncate(item, SUMMARY_MAX_CHARS),
17871
+ size_hint: "minimal"
17872
+ });
17873
+ }
17874
+ }
17875
+ }
17876
+ }
17877
+ return out;
17878
+ }
17879
+ return [];
17880
+ }
17881
+ function isCategoryValue(v) {
17882
+ return typeof v === "string" && CATEGORY_VALUES.includes(v);
17883
+ }
17884
+ function isSizeHintValue(v) {
17885
+ return typeof v === "string" && (v === "minimal" || v === "small" || v === "medium" || v === "large");
17886
+ }
17887
+ function categoryFromPolicyRuleId(ruleId) {
17888
+ const lower = ruleId.toLowerCase();
17889
+ if (lower.includes("credential") || lower.includes("broker_secret")) {
17890
+ return "credentials";
17891
+ }
17892
+ if (lower.includes("memory") || lower.includes("state_read")) {
17893
+ return "memory";
17894
+ }
17895
+ if (lower.includes("plan")) {
17896
+ return "plans";
17897
+ }
17898
+ if (lower.includes("export") || lower.includes("output")) {
17899
+ return "outputs";
17900
+ }
17901
+ if (lower.includes("audit")) {
17902
+ return "audit-refs";
17903
+ }
17904
+ return "other";
17905
+ }
17906
+ var SUMMARY_MAX_CHARS, CATEGORY_VALUES, CONTEXT_TRANSFER_AUDIT_OPS;
17907
+ var init_context_transfer_extractor = __esm({
17908
+ "src/coordination/context-transfer-extractor.ts"() {
17909
+ SUMMARY_MAX_CHARS = 240;
17910
+ CATEGORY_VALUES = [
17911
+ "memory",
17912
+ "credentials",
17913
+ "plans",
17914
+ "outputs",
17915
+ "audit-refs",
17916
+ "other"
17917
+ ];
17918
+ CONTEXT_TRANSFER_AUDIT_OPS = {
17919
+ DECODED: "operator_handoff_context_transfer_decoded"
17623
17920
  };
17624
17921
  }
17625
17922
  });
17923
+ function groupHandoffsIntoWorkflows(handoffs, opts) {
17924
+ if (handoffs.length === 0) return [];
17925
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
17926
+ const linkedGroups = /* @__PURE__ */ new Map();
17927
+ const unlinked = [];
17928
+ for (const h of handoffs) {
17929
+ if (h.workflow_link !== null && h.workflow_link.length > 0) {
17930
+ let bucket = linkedGroups.get(h.workflow_link);
17931
+ if (!bucket) {
17932
+ bucket = [];
17933
+ linkedGroups.set(h.workflow_link, bucket);
17934
+ }
17935
+ bucket.push(h);
17936
+ } else {
17937
+ unlinked.push(h);
17938
+ }
17939
+ }
17940
+ const sortedUnlinked = [...unlinked].sort(
17941
+ (a, b) => a.observed_at < b.observed_at ? -1 : 1
17942
+ );
17943
+ const heuristicChains = [];
17944
+ for (const h of sortedUnlinked) {
17945
+ const joinedIdx = findExtendableChain(heuristicChains, h);
17946
+ if (joinedIdx !== null) {
17947
+ heuristicChains[joinedIdx].push(h);
17948
+ } else {
17949
+ heuristicChains.push([h]);
17950
+ }
17951
+ }
17952
+ const workflows = [];
17953
+ for (const members of linkedGroups.values()) {
17954
+ workflows.push(materialize(members, now));
17955
+ }
17956
+ for (const members of heuristicChains) {
17957
+ workflows.push(materialize(members, now));
17958
+ }
17959
+ workflows.sort(
17960
+ (a, b) => a.last_activity_at < b.last_activity_at ? 1 : -1
17961
+ );
17962
+ return workflows;
17963
+ }
17964
+ function determineWorkflowState(members, now) {
17965
+ if (members.length === 0) return "unknown";
17966
+ const sorted = [...members].sort(
17967
+ (a, b) => a.observed_at < b.observed_at ? -1 : 1
17968
+ );
17969
+ const last = sorted[sorted.length - 1];
17970
+ const root = sorted[0];
17971
+ const lastMs = Date.parse(last.observed_at);
17972
+ if (!Number.isFinite(lastMs)) return "unknown";
17973
+ if (last.target_agent_id === OPERATOR_PSEUDO_AGENT) {
17974
+ return "completed";
17975
+ }
17976
+ if (sorted.length > CYCLE_COMPLETION_MIN_HOPS && last.target_agent_id === root.source_agent_id) {
17977
+ return "completed";
17978
+ }
17979
+ const ageMs = now.getTime() - lastMs;
17980
+ if (ageMs > STALL_THRESHOLD_MS) {
17981
+ return "stalled";
17982
+ }
17983
+ return "in_progress";
17984
+ }
17985
+ function workflowIdFromRoot(rootEntryId) {
17986
+ return createHash("sha256").update(`workflow:${rootEntryId}`).digest("hex").slice(0, 32);
17987
+ }
17988
+ function findExtendableChain(chains, h) {
17989
+ const hMs = Date.parse(h.observed_at);
17990
+ if (!Number.isFinite(hMs)) return null;
17991
+ let bestIdx = null;
17992
+ let bestGapMs = Number.POSITIVE_INFINITY;
17993
+ for (let i = 0; i < chains.length; i += 1) {
17994
+ const chain = chains[i];
17995
+ const last = chain[chain.length - 1];
17996
+ const lastMs = Date.parse(last.observed_at);
17997
+ if (!Number.isFinite(lastMs)) continue;
17998
+ const gapMs = Math.abs(hMs - lastMs);
17999
+ if (gapMs > HEURISTIC_WINDOW_MS) continue;
18000
+ if (!sharesAgent(last, h)) continue;
18001
+ if (gapMs < bestGapMs) {
18002
+ bestGapMs = gapMs;
18003
+ bestIdx = i;
18004
+ }
18005
+ }
18006
+ return bestIdx;
18007
+ }
18008
+ function sharesAgent(a, b) {
18009
+ return a.source_agent_id === b.source_agent_id || a.source_agent_id === b.target_agent_id || a.target_agent_id === b.source_agent_id || a.target_agent_id === b.target_agent_id;
18010
+ }
18011
+ function materialize(members, now) {
18012
+ const sorted = [...members].sort(
18013
+ (a, b) => a.observed_at < b.observed_at ? -1 : 1
18014
+ );
18015
+ const root = sorted[0];
18016
+ const last = sorted[sorted.length - 1];
18017
+ const involved = /* @__PURE__ */ new Set();
18018
+ for (const h of sorted) {
18019
+ if (h.source_agent_id) involved.add(h.source_agent_id);
18020
+ if (h.target_agent_id) involved.add(h.target_agent_id);
18021
+ }
18022
+ return {
18023
+ workflow_id: workflowIdFromRoot(root.entry_id),
18024
+ root_handoff: root,
18025
+ member_handoffs: sorted,
18026
+ state: determineWorkflowState(sorted, now),
18027
+ started_at: root.observed_at,
18028
+ last_activity_at: last.observed_at,
18029
+ involved_agents: [...involved].sort()
18030
+ };
18031
+ }
18032
+ var HEURISTIC_WINDOW_MS, STALL_THRESHOLD_MS, CYCLE_COMPLETION_MIN_HOPS;
18033
+ var init_workflow_grouper = __esm({
18034
+ "src/coordination/workflow-grouper.ts"() {
18035
+ init_handoff_log();
18036
+ HEURISTIC_WINDOW_MS = 5 * 60 * 1e3;
18037
+ STALL_THRESHOLD_MS = 2 * 60 * 60 * 1e3;
18038
+ CYCLE_COMPLETION_MIN_HOPS = 2;
18039
+ }
18040
+ });
17626
18041
 
17627
18042
  // src/coordination/handoff-routes.ts
17628
18043
  function writeJSON6(res, status, payload) {
@@ -17646,6 +18061,101 @@ function matchEntryRoute2(path) {
17646
18061
  if (rest.includes("/")) return null;
17647
18062
  return { entryId: decodeURIComponent(rest) };
17648
18063
  }
18064
+ function matchWorkflowRoute(path) {
18065
+ const prefix = `${COORDINATION_WORKFLOWS_PREFIX}/`;
18066
+ if (!path.startsWith(prefix)) return null;
18067
+ const rest = path.slice(prefix.length);
18068
+ if (rest.length === 0 || rest === "stream") return null;
18069
+ if (rest.includes("/")) return null;
18070
+ return { workflowId: decodeURIComponent(rest) };
18071
+ }
18072
+ async function computeWorkflowsAndTrackTransitions(deps) {
18073
+ const handoffs = await deps.handoffLog.query({ limit: 500 });
18074
+ const workflows = groupHandoffsIntoWorkflows(handoffs, {
18075
+ ...deps.now !== void 0 ? { now: deps.now() } : {}
18076
+ });
18077
+ const transitions = deps.workflowStateTracker ? deps.workflowStateTracker.observe(workflows) : [];
18078
+ for (const change of transitions) {
18079
+ deps.auditLog.append(
18080
+ "l2",
18081
+ COORDINATION_VIEW_AUDIT_OPS.WORKFLOW_STATE_CHANGED,
18082
+ deps.operatorId,
18083
+ {
18084
+ fortress_id: deps.handoffLog.getFortressId(),
18085
+ workflow_id: change.workflow_id,
18086
+ previous_state: change.previous_state,
18087
+ new_state: change.new_state
18088
+ }
18089
+ );
18090
+ }
18091
+ return { workflows, transitions };
18092
+ }
18093
+ function filterWorkflowList(workflows, opts) {
18094
+ let filtered = workflows;
18095
+ if (opts.state) {
18096
+ filtered = filtered.filter((w) => w.state === opts.state);
18097
+ }
18098
+ if (opts.agentId) {
18099
+ filtered = filtered.filter((w) => w.involved_agents.includes(opts.agentId));
18100
+ }
18101
+ if (opts.since) {
18102
+ filtered = filtered.filter((w) => w.last_activity_at >= opts.since);
18103
+ }
18104
+ return filtered.slice(0, opts.limit);
18105
+ }
18106
+ function isWorkflowState(value) {
18107
+ return value === "in_progress" || value === "completed" || value === "stalled" || value === "unknown";
18108
+ }
18109
+ async function handleWorkflowStream(deps, res) {
18110
+ res.writeHead(200, {
18111
+ "Content-Type": "text/event-stream",
18112
+ "Cache-Control": "no-cache, no-transform",
18113
+ Connection: "keep-alive",
18114
+ "X-Accel-Buffering": "no"
18115
+ });
18116
+ const initial = await computeWorkflowsAndTrackTransitions(deps);
18117
+ res.write(
18118
+ `event: workflow_snapshot
18119
+ data: ${JSON.stringify({ workflows: initial.workflows })}
18120
+
18121
+ `
18122
+ );
18123
+ if (initial.transitions.length > 0) {
18124
+ res.write(
18125
+ `event: workflow_state_changed
18126
+ data: ${JSON.stringify({ transitions: initial.transitions })}
18127
+
18128
+ `
18129
+ );
18130
+ }
18131
+ const unsubscribe = deps.events.subscribe(() => {
18132
+ void (async () => {
18133
+ try {
18134
+ const tick = await computeWorkflowsAndTrackTransitions(deps);
18135
+ res.write(
18136
+ `event: workflow_snapshot
18137
+ data: ${JSON.stringify({ workflows: tick.workflows })}
18138
+
18139
+ `
18140
+ );
18141
+ if (tick.transitions.length > 0) {
18142
+ res.write(
18143
+ `event: workflow_state_changed
18144
+ data: ${JSON.stringify({ transitions: tick.transitions })}
18145
+
18146
+ `
18147
+ );
18148
+ }
18149
+ } catch {
18150
+ }
18151
+ })();
18152
+ });
18153
+ const cleanup = () => {
18154
+ unsubscribe();
18155
+ };
18156
+ res.on("close", cleanup);
18157
+ res.on("error", cleanup);
18158
+ }
17649
18159
  async function handleStream3(deps, res) {
17650
18160
  res.writeHead(200, {
17651
18161
  "Content-Type": "text/event-stream",
@@ -17729,6 +18239,67 @@ async function handleCoordinationRoute(deps, req, res) {
17729
18239
  writeJSON6(res, 200, { ok: true, data: { entries } });
17730
18240
  return true;
17731
18241
  }
18242
+ if (method === "GET" && path === `${COORDINATION_WORKFLOWS_PREFIX}/stream`) {
18243
+ await handleWorkflowStream(deps, res);
18244
+ return true;
18245
+ }
18246
+ if (method === "GET" && path === COORDINATION_WORKFLOWS_PREFIX) {
18247
+ const limit = parseLimit4(
18248
+ url.searchParams.get("limit"),
18249
+ COORDINATION_LIST_DEFAULT_LIMIT,
18250
+ COORDINATION_LIST_MAX_LIMIT
18251
+ );
18252
+ const rawState = url.searchParams.get("state");
18253
+ const state = rawState && isWorkflowState(rawState) ? rawState : void 0;
18254
+ const since = url.searchParams.get("since") ?? void 0;
18255
+ const agentId = url.searchParams.get("agent_id") ?? void 0;
18256
+ const computed = await computeWorkflowsAndTrackTransitions(deps);
18257
+ const filtered = filterWorkflowList(computed.workflows, {
18258
+ ...state !== void 0 ? { state } : {},
18259
+ ...agentId !== void 0 ? { agentId } : {},
18260
+ ...since !== void 0 ? { since } : {},
18261
+ limit
18262
+ });
18263
+ deps.auditLog.append(
18264
+ "l2",
18265
+ COORDINATION_VIEW_AUDIT_OPS.WORKFLOW_VIEW_OPENED,
18266
+ deps.operatorId,
18267
+ {
18268
+ fortress_id: deps.handoffLog.getFortressId(),
18269
+ result_count: filtered.length,
18270
+ ...state !== void 0 ? { state } : {},
18271
+ ...agentId !== void 0 ? { agent_id: agentId } : {},
18272
+ ...since !== void 0 ? { since } : {}
18273
+ }
18274
+ );
18275
+ writeJSON6(res, 200, { ok: true, data: { workflows: filtered } });
18276
+ return true;
18277
+ }
18278
+ const workflowMatch = matchWorkflowRoute(path);
18279
+ if (method === "GET" && workflowMatch) {
18280
+ const computed = await computeWorkflowsAndTrackTransitions(deps);
18281
+ const wf = computed.workflows.find(
18282
+ (w) => w.workflow_id === workflowMatch.workflowId
18283
+ );
18284
+ if (!wf) {
18285
+ writeJSON6(res, 404, { ok: false, error: "not_found" });
18286
+ return true;
18287
+ }
18288
+ deps.auditLog.append(
18289
+ "l2",
18290
+ COORDINATION_VIEW_AUDIT_OPS.WORKFLOW_DRILLED,
18291
+ deps.operatorId,
18292
+ {
18293
+ fortress_id: deps.handoffLog.getFortressId(),
18294
+ workflow_id: wf.workflow_id,
18295
+ state: wf.state,
18296
+ member_count: wf.member_handoffs.length,
18297
+ involved_agent_count: wf.involved_agents.length
18298
+ }
18299
+ );
18300
+ writeJSON6(res, 200, { ok: true, data: { workflow: wf } });
18301
+ return true;
18302
+ }
17732
18303
  const entryMatch = matchEntryRoute2(path);
17733
18304
  if (method === "GET" && entryMatch) {
17734
18305
  const detail = await deps.handoffLog.getEntry(entryMatch.entryId);
@@ -17748,7 +18319,29 @@ async function handleCoordinationRoute(deps, req, res) {
17748
18319
  target_agent_id: detail.entry.target_agent_id
17749
18320
  }
17750
18321
  );
17751
- writeJSON6(res, 200, { ok: true, data: detail });
18322
+ let breakdown = null;
18323
+ try {
18324
+ breakdown = await extractContextTransferBreakdown(
18325
+ detail,
18326
+ deps.contextTransfer ?? {}
18327
+ );
18328
+ deps.auditLog.append(
18329
+ "l2",
18330
+ CONTEXT_TRANSFER_AUDIT_OPS.DECODED,
18331
+ deps.operatorId,
18332
+ {
18333
+ fortress_id: deps.handoffLog.getFortressId(),
18334
+ entry_id: detail.entry.entry_id,
18335
+ extractor_path: breakdown.source,
18336
+ confidence: breakdown.confidence,
18337
+ transferred_count: breakdown.transferred.length,
18338
+ withheld_count: breakdown.withheld.length
18339
+ }
18340
+ );
18341
+ } catch {
18342
+ }
18343
+ const responseData = breakdown !== null ? { ...detail, context_transfer_breakdown: breakdown } : detail;
18344
+ writeJSON6(res, 200, { ok: true, data: responseData });
17752
18345
  return true;
17753
18346
  }
17754
18347
  writeJSON6(res, 404, { ok: false, error: "not_found", path });
@@ -17759,13 +18352,16 @@ async function handleCoordinationRoute(deps, req, res) {
17759
18352
  return true;
17760
18353
  }
17761
18354
  }
17762
- var COORDINATION_API_PREFIX, COORDINATION_HANDOFFS_PREFIX, COORDINATION_LIST_DEFAULT_LIMIT, COORDINATION_LIST_MAX_LIMIT, HandoffEventBridge;
18355
+ var COORDINATION_API_PREFIX, COORDINATION_HANDOFFS_PREFIX, COORDINATION_WORKFLOWS_PREFIX, COORDINATION_LIST_DEFAULT_LIMIT, COORDINATION_LIST_MAX_LIMIT, HandoffEventBridge;
17763
18356
  var init_handoff_routes = __esm({
17764
18357
  "src/coordination/handoff-routes.ts"() {
17765
18358
  init_auth_middleware();
17766
18359
  init_handoff_log();
18360
+ init_context_transfer_extractor();
18361
+ init_workflow_grouper();
17767
18362
  COORDINATION_API_PREFIX = "/api/coordination";
17768
18363
  COORDINATION_HANDOFFS_PREFIX = "/api/coordination/handoffs";
18364
+ COORDINATION_WORKFLOWS_PREFIX = "/api/coordination/workflows";
17769
18365
  COORDINATION_LIST_DEFAULT_LIMIT = 50;
17770
18366
  COORDINATION_LIST_MAX_LIMIT = 500;
17771
18367
  HandoffEventBridge = class {
@@ -17884,6 +18480,8 @@ var init_dashboard = __esm({
17884
18480
  */
17885
18481
  handoffLog = null;
17886
18482
  handoffEventBridge = null;
18483
+ handoffContextTransfer = null;
18484
+ workflowStateTracker = null;
17887
18485
  handoffAuditLog = null;
17888
18486
  handoffOperatorId = null;
17889
18487
  constructor(config) {
@@ -17964,6 +18562,8 @@ var init_dashboard = __esm({
17964
18562
  this.handoffEventBridge = opts.eventBridge ?? null;
17965
18563
  this.handoffAuditLog = opts.auditLog ?? null;
17966
18564
  this.handoffOperatorId = opts.operatorId ?? null;
18565
+ this.handoffContextTransfer = opts.contextTransfer ?? null;
18566
+ this.workflowStateTracker = opts.workflowStateTracker ?? null;
17967
18567
  }
17968
18568
  /**
17969
18569
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
@@ -18021,7 +18621,9 @@ var init_dashboard = __esm({
18021
18621
  handoffLog: this.handoffLog,
18022
18622
  auditLog: this.handoffAuditLog,
18023
18623
  operatorId: this.handoffOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
18024
- events: this.handoffEventBridge
18624
+ events: this.handoffEventBridge,
18625
+ ...this.handoffContextTransfer !== null ? { contextTransfer: this.handoffContextTransfer } : {},
18626
+ ...this.workflowStateTracker !== null ? { workflowStateTracker: this.workflowStateTracker } : {}
18025
18627
  },
18026
18628
  req,
18027
18629
  res
@@ -22305,46 +22907,346 @@ var init_sentinel_dispatcher = __esm({
22305
22907
  }
22306
22908
  });
22307
22909
 
22308
- // src/anomaly-detection/types.ts
22309
- var init_types4 = __esm({
22310
- "src/anomaly-detection/types.ts"() {
22311
- }
22312
- });
22313
- var ANOMALY_AUDIT_OPS, DEFAULT_TICK_INTERVAL_MS2, AnomalyPipelineDispatcher;
22314
- var init_anomaly_pipeline = __esm({
22315
- "src/anomaly-detection/anomaly-pipeline.ts"() {
22316
- init_types4();
22317
- ANOMALY_AUDIT_OPS = {
22318
- DETECTOR_REGISTERED: "anomaly_detector_registered",
22319
- DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
22320
- FINDING_EMITTED: "anomaly_finding_emitted",
22321
- EVALUATION_FAILED: "anomaly_evaluation_failed",
22322
- TRAINING_COMPLETED: "anomaly_training_completed",
22323
- TRAINING_FAILED: "anomaly_training_failed"
22324
- };
22325
- DEFAULT_TICK_INTERVAL_MS2 = 6e4;
22326
- AnomalyPipelineDispatcher = class {
22327
- findingStore;
22328
- auditLog;
22910
+ // src/anomaly-detection/classifier-state-store.ts
22911
+ function stateKey(classifierId, agentId) {
22912
+ return `${ANOMALY_CLASSIFIER_STATE_KEY_PREFIX}${classifierId}.${agentId}`;
22913
+ }
22914
+ function aadFor(classifierId, agentId) {
22915
+ return `${classifierId}|${agentId}`;
22916
+ }
22917
+ var ANOMALY_CLASSIFIER_STATE_NAMESPACE, ANOMALY_CLASSIFIER_STATE_KEY_PREFIX, HKDF_INFO3, MAX_STATE_BYTES, ClassifierStateStore;
22918
+ var init_classifier_state_store = __esm({
22919
+ "src/anomaly-detection/classifier-state-store.ts"() {
22920
+ init_encryption();
22921
+ init_key_derivation();
22922
+ init_encoding();
22923
+ ANOMALY_CLASSIFIER_STATE_NAMESPACE = "_anomaly_classifier_state";
22924
+ ANOMALY_CLASSIFIER_STATE_KEY_PREFIX = "state.";
22925
+ HKDF_INFO3 = "l2-anomaly-classifier-state-v1";
22926
+ MAX_STATE_BYTES = 256 * 1024;
22927
+ ClassifierStateStore = class {
22329
22928
  storage;
22330
- masterKey;
22929
+ encryptionKey;
22331
22930
  fortressId;
22332
- identityId;
22333
22931
  now;
22334
- tickIntervalMs;
22335
- detectors = /* @__PURE__ */ new Map();
22336
- listeners = /* @__PURE__ */ new Set();
22337
- tickTimer = null;
22338
- tickInFlight = false;
22339
- constructor(deps) {
22340
- this.findingStore = deps.findingStore;
22341
- this.auditLog = deps.auditLog;
22342
- this.storage = deps.storage;
22343
- this.masterKey = deps.masterKey;
22344
- this.fortressId = deps.fortressId;
22345
- this.identityId = deps.identityId;
22346
- this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
22347
- this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS2;
22932
+ constructor(opts) {
22933
+ this.storage = opts.storage;
22934
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
22935
+ this.fortressId = opts.fortressId;
22936
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
22937
+ }
22938
+ async saveState(classifierId, agentId, state) {
22939
+ const persisted = {
22940
+ version: 1,
22941
+ classifier_id: classifierId,
22942
+ agent_id: agentId,
22943
+ fortress_id: this.fortressId,
22944
+ saved_at: this.now().toISOString(),
22945
+ state
22946
+ };
22947
+ const aadString = aadFor(classifierId, agentId);
22948
+ const aad = stringToBytes(aadString);
22949
+ const plaintext = stringToBytes(JSON.stringify(persisted));
22950
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
22951
+ await this.storage.write(
22952
+ ANOMALY_CLASSIFIER_STATE_NAMESPACE,
22953
+ stateKey(classifierId, agentId),
22954
+ stringToBytes(JSON.stringify(envelope))
22955
+ );
22956
+ }
22957
+ async loadState(classifierId, agentId) {
22958
+ const key = stateKey(classifierId, agentId);
22959
+ let raw;
22960
+ try {
22961
+ raw = await this.storage.read(
22962
+ ANOMALY_CLASSIFIER_STATE_NAMESPACE,
22963
+ key
22964
+ );
22965
+ } catch {
22966
+ return null;
22967
+ }
22968
+ if (!raw) return null;
22969
+ if (raw.length > MAX_STATE_BYTES) return null;
22970
+ try {
22971
+ const aad = stringToBytes(aadFor(classifierId, agentId));
22972
+ const envelope = JSON.parse(bytesToString(raw));
22973
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
22974
+ const persisted = JSON.parse(
22975
+ bytesToString(plaintext)
22976
+ );
22977
+ if (persisted.version !== 1) return null;
22978
+ if (persisted.classifier_id !== classifierId) return null;
22979
+ if (persisted.agent_id !== agentId) return null;
22980
+ if (persisted.fortress_id !== this.fortressId) return null;
22981
+ return persisted.state;
22982
+ } catch {
22983
+ return null;
22984
+ }
22985
+ }
22986
+ /** Delete a single classifier-agent state record. */
22987
+ async deleteState(classifierId, agentId) {
22988
+ const key = stateKey(classifierId, agentId);
22989
+ const existed = await this.storage.exists(
22990
+ ANOMALY_CLASSIFIER_STATE_NAMESPACE,
22991
+ key
22992
+ );
22993
+ if (!existed) return false;
22994
+ try {
22995
+ await this.storage.delete(ANOMALY_CLASSIFIER_STATE_NAMESPACE, key);
22996
+ } catch {
22997
+ return false;
22998
+ }
22999
+ return true;
23000
+ }
23001
+ /**
23002
+ * List the (classifier_id, agent_id) tuples currently persisted.
23003
+ * Returns the agent ids for one classifier when classifierId is
23004
+ * given.
23005
+ */
23006
+ async listAgents(classifierId) {
23007
+ const metas = await this.storage.list(
23008
+ ANOMALY_CLASSIFIER_STATE_NAMESPACE,
23009
+ ANOMALY_CLASSIFIER_STATE_KEY_PREFIX
23010
+ );
23011
+ const prefix = `${ANOMALY_CLASSIFIER_STATE_KEY_PREFIX}${classifierId}.`;
23012
+ const out = [];
23013
+ for (const meta of metas) {
23014
+ if (!meta.key.startsWith(prefix)) continue;
23015
+ out.push(meta.key.slice(prefix.length));
23016
+ }
23017
+ return out;
23018
+ }
23019
+ };
23020
+ }
23021
+ });
23022
+
23023
+ // src/anomaly-detection/classifiers/cusum.ts
23024
+ var CUSUM_CLASSIFIER_ID;
23025
+ var init_cusum = __esm({
23026
+ "src/anomaly-detection/classifiers/cusum.ts"() {
23027
+ init_classifier_state_store();
23028
+ CUSUM_CLASSIFIER_ID = "cusum";
23029
+ }
23030
+ });
23031
+
23032
+ // src/anomaly-detection/classifiers/psi.ts
23033
+ var PSI_CLASSIFIER_ID;
23034
+ var init_psi = __esm({
23035
+ "src/anomaly-detection/classifiers/psi.ts"() {
23036
+ init_classifier_state_store();
23037
+ PSI_CLASSIFIER_ID = "psi";
23038
+ }
23039
+ });
23040
+
23041
+ // src/anomaly-detection/types.ts
23042
+ function severityFromAnomalyScore(score) {
23043
+ if (!Number.isFinite(score)) return null;
23044
+ if (score < 1) return null;
23045
+ if (score < 3) return "info";
23046
+ if (score < 6) return "warn";
23047
+ return "alert";
23048
+ }
23049
+ function buildAnomalyFinding(detector, classifier, vector, prediction, severity) {
23050
+ const summary = formatAnomalySummary(
23051
+ detector,
23052
+ classifier,
23053
+ vector,
23054
+ prediction,
23055
+ severity
23056
+ );
23057
+ return {
23058
+ finding_id: "",
23059
+ sentinel_id: `${ANOMALY_SENTINEL_ID_PREFIX}${detector.detectorId}`,
23060
+ severity,
23061
+ summary,
23062
+ details: {
23063
+ detector_id: detector.detectorId,
23064
+ classifier_id: classifier.classifierId,
23065
+ anomaly_score: prediction.anomaly_score,
23066
+ window_label: vector.window_label,
23067
+ observed_features: vector.features,
23068
+ feature_contributions: prediction.feature_contributions,
23069
+ explanation: prediction.explanation
23070
+ },
23071
+ observed_at: vector.observed_at,
23072
+ agent_id: vector.agent_id,
23073
+ evidence_audit_ids: [],
23074
+ fortress_id: ""
23075
+ };
23076
+ }
23077
+ function formatAnomalySummary(detector, classifier, vector, prediction, severity) {
23078
+ const top = prediction.explanation.slice(0, 3).join("; ");
23079
+ return `${detector.detectorId}/${classifier.classifierId} ${severity}: agent ${vector.agent_id} drifted ${prediction.anomaly_score.toFixed(2)} sigma from baseline. Top contributors: ${top || "(none)"}.`;
23080
+ }
23081
+ var AnomalyDetector, ANOMALY_SENTINEL_ID_PREFIX;
23082
+ var init_types4 = __esm({
23083
+ "src/anomaly-detection/types.ts"() {
23084
+ AnomalyDetector = class {
23085
+ /**
23086
+ * Additional classifiers attached post-construction. Keyed by
23087
+ * classifierId so subscribe/unsubscribe is idempotent. Primary
23088
+ * `classifier` is NOT stored here.
23089
+ */
23090
+ additionalClassifiers = /* @__PURE__ */ new Map();
23091
+ /**
23092
+ * Attach an additional classifier. Idempotent: a second call with
23093
+ * the same classifierId returns false. The primary classifier
23094
+ * cannot be re-attached as additional (returns false). The
23095
+ * dispatcher emits ANOMALY_CLASSIFIER_SUBSCRIBED on success.
23096
+ */
23097
+ addClassifier(classifier) {
23098
+ if (classifier.classifierId === this.classifier.classifierId) return false;
23099
+ if (this.additionalClassifiers.has(classifier.classifierId)) return false;
23100
+ this.additionalClassifiers.set(classifier.classifierId, classifier);
23101
+ return true;
23102
+ }
23103
+ /**
23104
+ * Detach an additional classifier by id. Cannot remove the primary
23105
+ * (returns false). Returns true when an existing additional
23106
+ * classifier was removed. The dispatcher emits
23107
+ * ANOMALY_CLASSIFIER_UNSUBSCRIBED on success.
23108
+ */
23109
+ removeClassifier(classifierId) {
23110
+ if (classifierId === this.classifier.classifierId) return false;
23111
+ return this.additionalClassifiers.delete(classifierId);
23112
+ }
23113
+ /** List every classifier id attached: primary first, then additionals. */
23114
+ listClassifierIds() {
23115
+ return [
23116
+ this.classifier.classifierId,
23117
+ ...this.additionalClassifiers.keys()
23118
+ ];
23119
+ }
23120
+ /**
23121
+ * Return every attached classifier: primary first, then additionals
23122
+ * in insertion order. Used by evaluate() and the dispatcher's train
23123
+ * + audit emission.
23124
+ */
23125
+ getAllClassifiers() {
23126
+ return [this.classifier, ...this.additionalClassifiers.values()];
23127
+ }
23128
+ /**
23129
+ * Bind the detector to a fortress context. Default stores it on
23130
+ * `this`; subclasses with priming logic override.
23131
+ */
23132
+ async subscribe(context) {
23133
+ this.context = context;
23134
+ }
23135
+ async unsubscribe() {
23136
+ this.context = void 0;
23137
+ this.additionalClassifiers.clear();
23138
+ }
23139
+ /**
23140
+ * One evaluation pass. Default impl: extract -> for each classifier
23141
+ * attached, predict (drift against that classifier's prior
23142
+ * baseline) -> observe (only when the prediction is in-baseline,
23143
+ * so outliers do not contaminate the rolling baseline and pull
23144
+ * future predictions toward themselves) -> emit findings above
23145
+ * threshold. Multi-classifier evaluation is per-classifier: each
23146
+ * decides independently whether to absorb or emit. Subclasses with
23147
+ * custom routing override.
23148
+ *
23149
+ * Predict-then-observe (with conditional observe) is the standard
23150
+ * online anomaly-detection pattern. Chi-1 spawn prompt called for
23151
+ * observe-then-predict; CTO call: changed to predict-then-observe
23152
+ * because observe-then-predict measures the sample against itself
23153
+ * after one-sample contamination, which is structurally incorrect
23154
+ * for drift detection. Chi-2 preserves that invariant on a per-
23155
+ * classifier basis (each classifier's observe is conditional on its
23156
+ * own predict result).
23157
+ */
23158
+ async evaluate() {
23159
+ const ctx = this.requireContext();
23160
+ const vectors = await this.featureExtract(ctx);
23161
+ const findings = [];
23162
+ const classifiers = this.getAllClassifiers();
23163
+ for (const vector of vectors) {
23164
+ for (const classifier of classifiers) {
23165
+ const prediction = await classifier.predict(vector);
23166
+ if (!prediction.baseline_ready) {
23167
+ await classifier.observe(vector);
23168
+ continue;
23169
+ }
23170
+ const severity = severityFromAnomalyScore(prediction.anomaly_score);
23171
+ if (severity === null) {
23172
+ await classifier.observe(vector);
23173
+ continue;
23174
+ }
23175
+ findings.push(
23176
+ buildAnomalyFinding(this, classifier, vector, prediction, severity)
23177
+ );
23178
+ }
23179
+ }
23180
+ return findings;
23181
+ }
23182
+ context;
23183
+ requireContext() {
23184
+ if (!this.context) {
23185
+ throw new Error(
23186
+ `anomaly-detector ${this.detectorId}: evaluate() called before subscribe()`
23187
+ );
23188
+ }
23189
+ return this.context;
23190
+ }
23191
+ };
23192
+ ANOMALY_SENTINEL_ID_PREFIX = "anomaly:";
23193
+ }
23194
+ });
23195
+ function classifierSpecificAuditOp(classifierId) {
23196
+ if (classifierId === null) return null;
23197
+ if (classifierId === CUSUM_CLASSIFIER_ID) {
23198
+ return ANOMALY_AUDIT_OPS.CUSUM_DRIFT_DETECTED;
23199
+ }
23200
+ if (classifierId === PSI_CLASSIFIER_ID) {
23201
+ return ANOMALY_AUDIT_OPS.PSI_DISTRIBUTION_SHIFT_DETECTED;
23202
+ }
23203
+ return null;
23204
+ }
23205
+ var ANOMALY_AUDIT_OPS, DEFAULT_TICK_INTERVAL_MS2, AnomalyPipelineDispatcher;
23206
+ var init_anomaly_pipeline = __esm({
23207
+ "src/anomaly-detection/anomaly-pipeline.ts"() {
23208
+ init_cusum();
23209
+ init_psi();
23210
+ init_types4();
23211
+ ANOMALY_AUDIT_OPS = {
23212
+ DETECTOR_REGISTERED: "anomaly_detector_registered",
23213
+ DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
23214
+ FINDING_EMITTED: "anomaly_finding_emitted",
23215
+ EVALUATION_FAILED: "anomaly_evaluation_failed",
23216
+ TRAINING_COMPLETED: "anomaly_training_completed",
23217
+ TRAINING_FAILED: "anomaly_training_failed",
23218
+ /** Chi-2: a classifier was attached to an existing detector. */
23219
+ CLASSIFIER_SUBSCRIBED: "anomaly_classifier_subscribed",
23220
+ /** Chi-2: a classifier was detached from an existing detector. */
23221
+ CLASSIFIER_UNSUBSCRIBED: "anomaly_classifier_unsubscribed",
23222
+ /** Chi-2: CUSUM-flagged mean-shift drift on a per-agent feature. */
23223
+ CUSUM_DRIFT_DETECTED: "anomaly_cusum_drift_detected",
23224
+ /** Chi-2: PSI-flagged distribution shift on a per-agent feature. */
23225
+ PSI_DISTRIBUTION_SHIFT_DETECTED: "anomaly_psi_distribution_shift_detected"
23226
+ };
23227
+ DEFAULT_TICK_INTERVAL_MS2 = 6e4;
23228
+ AnomalyPipelineDispatcher = class {
23229
+ findingStore;
23230
+ auditLog;
23231
+ storage;
23232
+ masterKey;
23233
+ fortressId;
23234
+ identityId;
23235
+ now;
23236
+ tickIntervalMs;
23237
+ detectors = /* @__PURE__ */ new Map();
23238
+ listeners = /* @__PURE__ */ new Set();
23239
+ tickTimer = null;
23240
+ tickInFlight = false;
23241
+ constructor(deps) {
23242
+ this.findingStore = deps.findingStore;
23243
+ this.auditLog = deps.auditLog;
23244
+ this.storage = deps.storage;
23245
+ this.masterKey = deps.masterKey;
23246
+ this.fortressId = deps.fortressId;
23247
+ this.identityId = deps.identityId;
23248
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
23249
+ this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS2;
22348
23250
  }
22349
23251
  onEvent(listener) {
22350
23252
  this.listeners.add(listener);
@@ -22411,34 +23313,37 @@ var init_anomaly_pipeline = __esm({
22411
23313
  const stamped = await this.routeFinding(detectorId, raw);
22412
23314
  findings.push(stamped);
22413
23315
  }
22414
- try {
22415
- const trainingResult = await detector.classifier.train();
22416
- this.auditLog.append(
22417
- "l2",
22418
- ANOMALY_AUDIT_OPS.TRAINING_COMPLETED,
22419
- this.identityId,
22420
- {
22421
- detector_id: detectorId,
22422
- classifier_id: detector.classifier.classifierId,
22423
- trained_at: trainingResult.trained_at,
22424
- sample_count: trainingResult.sample_count,
22425
- agent_count: trainingResult.agent_count,
22426
- fortress_id: this.fortressId
22427
- }
22428
- );
22429
- } catch (trainErr) {
22430
- const message = trainErr instanceof Error ? trainErr.message : String(trainErr);
22431
- this.auditLog.append(
22432
- "l2",
22433
- ANOMALY_AUDIT_OPS.TRAINING_FAILED,
22434
- this.identityId,
22435
- {
22436
- detector_id: detectorId,
22437
- error_message: message,
22438
- fortress_id: this.fortressId
22439
- },
22440
- "failure"
22441
- );
23316
+ for (const classifier of detector.getAllClassifiers()) {
23317
+ try {
23318
+ const trainingResult = await classifier.train();
23319
+ this.auditLog.append(
23320
+ "l2",
23321
+ ANOMALY_AUDIT_OPS.TRAINING_COMPLETED,
23322
+ this.identityId,
23323
+ {
23324
+ detector_id: detectorId,
23325
+ classifier_id: classifier.classifierId,
23326
+ trained_at: trainingResult.trained_at,
23327
+ sample_count: trainingResult.sample_count,
23328
+ agent_count: trainingResult.agent_count,
23329
+ fortress_id: this.fortressId
23330
+ }
23331
+ );
23332
+ } catch (trainErr) {
23333
+ const message = trainErr instanceof Error ? trainErr.message : String(trainErr);
23334
+ this.auditLog.append(
23335
+ "l2",
23336
+ ANOMALY_AUDIT_OPS.TRAINING_FAILED,
23337
+ this.identityId,
23338
+ {
23339
+ detector_id: detectorId,
23340
+ classifier_id: classifier.classifierId,
23341
+ error_message: message,
23342
+ fortress_id: this.fortressId
23343
+ },
23344
+ "failure"
23345
+ );
23346
+ }
22442
23347
  }
22443
23348
  } catch (err) {
22444
23349
  const message = err instanceof Error ? err.message : String(err);
@@ -22501,6 +23406,7 @@ var init_anomaly_pipeline = __esm({
22501
23406
  observed_at: raw.observed_at || this.now().toISOString()
22502
23407
  };
22503
23408
  await this.findingStore.saveFinding(stamped);
23409
+ const classifierId = stamped.details["classifier_id"] ?? null;
22504
23410
  this.auditLog.append(
22505
23411
  "l2",
22506
23412
  ANOMALY_AUDIT_OPS.FINDING_EMITTED,
@@ -22510,13 +23416,79 @@ var init_anomaly_pipeline = __esm({
22510
23416
  finding_id: stamped.finding_id,
22511
23417
  severity: stamped.severity,
22512
23418
  anomaly_score: stamped.details["anomaly_score"] ?? null,
23419
+ ...classifierId !== null ? { classifier_id: classifierId } : {},
22513
23420
  ...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
22514
23421
  fortress_id: this.fortressId
22515
23422
  }
22516
23423
  );
23424
+ const specificOp = classifierSpecificAuditOp(classifierId);
23425
+ if (specificOp !== null) {
23426
+ this.auditLog.append("l2", specificOp, this.identityId, {
23427
+ detector_id: detectorId,
23428
+ finding_id: stamped.finding_id,
23429
+ severity: stamped.severity,
23430
+ anomaly_score: stamped.details["anomaly_score"] ?? null,
23431
+ ...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
23432
+ fortress_id: this.fortressId
23433
+ });
23434
+ }
22517
23435
  this.emit({ type: "finding", finding: stamped });
22518
23436
  return stamped;
22519
23437
  }
23438
+ /**
23439
+ * Chi-2: attach an additional classifier to an already-registered
23440
+ * detector. Emits ANOMALY_CLASSIFIER_SUBSCRIBED on success. The
23441
+ * factory is called with the fortress AnomalyContext so the
23442
+ * classifier can build its own state-store binding. Idempotent: a
23443
+ * second call with the same classifierId returns false.
23444
+ */
23445
+ async addClassifierToDetector(detectorId, factory) {
23446
+ const detector = this.detectors.get(detectorId);
23447
+ if (!detector) return false;
23448
+ const context = {
23449
+ fortressId: this.fortressId,
23450
+ auditLog: this.auditLog,
23451
+ storage: this.storage,
23452
+ masterKey: this.masterKey,
23453
+ now: this.now
23454
+ };
23455
+ const classifier = factory(context);
23456
+ const added = detector.addClassifier(classifier);
23457
+ if (!added) return false;
23458
+ this.auditLog.append(
23459
+ "l2",
23460
+ ANOMALY_AUDIT_OPS.CLASSIFIER_SUBSCRIBED,
23461
+ this.identityId,
23462
+ {
23463
+ detector_id: detectorId,
23464
+ classifier_id: classifier.classifierId,
23465
+ fortress_id: this.fortressId
23466
+ }
23467
+ );
23468
+ return true;
23469
+ }
23470
+ /**
23471
+ * Chi-2: detach an additional classifier from an already-registered
23472
+ * detector. Emits ANOMALY_CLASSIFIER_UNSUBSCRIBED on success. The
23473
+ * primary classifier cannot be detached (returns false).
23474
+ */
23475
+ async removeClassifierFromDetector(detectorId, classifierId) {
23476
+ const detector = this.detectors.get(detectorId);
23477
+ if (!detector) return false;
23478
+ const removed = detector.removeClassifier(classifierId);
23479
+ if (!removed) return false;
23480
+ this.auditLog.append(
23481
+ "l2",
23482
+ ANOMALY_AUDIT_OPS.CLASSIFIER_UNSUBSCRIBED,
23483
+ this.identityId,
23484
+ {
23485
+ detector_id: detectorId,
23486
+ classifier_id: classifierId,
23487
+ fortress_id: this.fortressId
23488
+ }
23489
+ );
23490
+ return true;
23491
+ }
22520
23492
  emit(event) {
22521
23493
  for (const listener of this.listeners) {
22522
23494
  try {
@@ -22529,6 +23501,75 @@ var init_anomaly_pipeline = __esm({
22529
23501
  }
22530
23502
  });
22531
23503
 
23504
+ // src/coordination/workflow-state-tracker.ts
23505
+ var WorkflowStateTracker;
23506
+ var init_workflow_state_tracker = __esm({
23507
+ "src/coordination/workflow-state-tracker.ts"() {
23508
+ WorkflowStateTracker = class {
23509
+ states = /* @__PURE__ */ new Map();
23510
+ now;
23511
+ constructor(opts) {
23512
+ this.now = opts?.now ?? (() => /* @__PURE__ */ new Date());
23513
+ }
23514
+ /**
23515
+ * Diff the supplied workflow list against the last-observed states.
23516
+ * Returns the set of transitions detected this call; the tracker
23517
+ * mutates its internal map to reflect the new states.
23518
+ *
23519
+ * Transitions emitted:
23520
+ * - First observation of a workflow (`previous_state` is the
23521
+ * sentinel `unobserved`). Lets the route handler audit-emit
23522
+ * the initial state so the operator sees workflows as they
23523
+ * surface, not only when they change.
23524
+ * - Subsequent observation where `previous_state !== new_state`.
23525
+ */
23526
+ observe(workflows) {
23527
+ const out = [];
23528
+ const observedAt = this.now().toISOString();
23529
+ for (const wf of workflows) {
23530
+ const prior = this.states.get(wf.workflow_id);
23531
+ if (prior === void 0) {
23532
+ out.push({
23533
+ workflow_id: wf.workflow_id,
23534
+ previous_state: "unobserved",
23535
+ new_state: wf.state,
23536
+ observed_at: observedAt
23537
+ });
23538
+ this.states.set(wf.workflow_id, wf.state);
23539
+ continue;
23540
+ }
23541
+ if (prior !== wf.state) {
23542
+ out.push({
23543
+ workflow_id: wf.workflow_id,
23544
+ previous_state: prior,
23545
+ new_state: wf.state,
23546
+ observed_at: observedAt
23547
+ });
23548
+ this.states.set(wf.workflow_id, wf.state);
23549
+ }
23550
+ }
23551
+ return out;
23552
+ }
23553
+ /**
23554
+ * Drop a workflow's recorded state. Surfaced for tests + future
23555
+ * "operator dismissed this workflow" affordance; not currently
23556
+ * called by the production wiring.
23557
+ */
23558
+ forget(workflowId) {
23559
+ this.states.delete(workflowId);
23560
+ }
23561
+ /** Reset the tracker. Tests use this between runs. */
23562
+ reset() {
23563
+ this.states.clear();
23564
+ }
23565
+ /** Read-only view of the current snapshot. Useful for diagnostics. */
23566
+ snapshot() {
23567
+ return new Map(this.states);
23568
+ }
23569
+ };
23570
+ }
23571
+ });
23572
+
22532
23573
  // src/sentinel/sentinel.ts
22533
23574
  var Sentinel;
22534
23575
  var init_sentinel = __esm({
@@ -38240,7 +39281,7 @@ ${runningLines.join("\n")}`;
38240
39281
  function chatStorageKey(surface, threadKey) {
38241
39282
  return `${surface}.${threadKey}`;
38242
39283
  }
38243
- var OPERATOR_CHAT_NAMESPACE, HKDF_INFO3, OperatorChatStore;
39284
+ var OPERATOR_CHAT_NAMESPACE, HKDF_INFO4, OperatorChatStore;
38244
39285
  var init_operator_chat_store = __esm({
38245
39286
  "src/chat/operator-chat-store.ts"() {
38246
39287
  init_encryption();
@@ -38248,13 +39289,13 @@ var init_operator_chat_store = __esm({
38248
39289
  init_encoding();
38249
39290
  init_operator_chat_types();
38250
39291
  OPERATOR_CHAT_NAMESPACE = "_chat";
38251
- HKDF_INFO3 = "operator-chat-store-v1";
39292
+ HKDF_INFO4 = "operator-chat-store-v1";
38252
39293
  OperatorChatStore = class {
38253
39294
  storage;
38254
39295
  encryptionKey;
38255
39296
  constructor(storage, masterKey) {
38256
39297
  this.storage = storage;
38257
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
39298
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
38258
39299
  }
38259
39300
  /**
38260
39301
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -38350,7 +39391,7 @@ function lastTurnId(bundle) {
38350
39391
  }
38351
39392
  return max;
38352
39393
  }
38353
- var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO4, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
39394
+ var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO5, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
38354
39395
  var init_concierge_memory_store = __esm({
38355
39396
  "src/chat/concierge-memory-store.ts"() {
38356
39397
  init_encryption();
@@ -38358,7 +39399,7 @@ var init_concierge_memory_store = __esm({
38358
39399
  init_encoding();
38359
39400
  CONCIERGE_MEMORY_NAMESPACE = "_chat";
38360
39401
  CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
38361
- HKDF_INFO4 = "concierge-memory-store-v1";
39402
+ HKDF_INFO5 = "concierge-memory-store-v1";
38362
39403
  DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
38363
39404
  MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
38364
39405
  ConciergeMemoryStore = class {
@@ -38369,7 +39410,7 @@ var init_concierge_memory_store = __esm({
38369
39410
  locks;
38370
39411
  constructor(opts) {
38371
39412
  this.storage = opts.storage;
38372
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
39413
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO5);
38373
39414
  this.fortressId = opts.fortressId;
38374
39415
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
38375
39416
  this.locks = /* @__PURE__ */ new Map();
@@ -39018,7 +40059,7 @@ var init_defaults = __esm({
39018
40059
  });
39019
40060
 
39020
40061
  // src/intelligence/policy-store.ts
39021
- var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO5, IntelligenceConfigStore;
40062
+ var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO6, IntelligenceConfigStore;
39022
40063
  var init_policy_store = __esm({
39023
40064
  "src/intelligence/policy-store.ts"() {
39024
40065
  init_encryption();
@@ -39027,13 +40068,13 @@ var init_policy_store = __esm({
39027
40068
  init_defaults();
39028
40069
  INTELLIGENCE_NAMESPACE = "_intelligence";
39029
40070
  SUBSTRATE_CONFIG_KEY = "substrate-config";
39030
- HKDF_INFO5 = "intelligence-substrate-config";
40071
+ HKDF_INFO6 = "intelligence-substrate-config";
39031
40072
  IntelligenceConfigStore = class {
39032
40073
  storage;
39033
40074
  encryptionKey;
39034
40075
  constructor(storage, masterKey) {
39035
40076
  this.storage = storage;
39036
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
40077
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO6);
39037
40078
  }
39038
40079
  /**
39039
40080
  * Load the operator's substrate config from disk. Returns the config
@@ -39826,17 +40867,152 @@ ${redactedItems.map((r) => `- ${r.redacted}`).join("\n")}`,
39826
40867
  }
39827
40868
  });
39828
40869
 
39829
- // src/intelligence/substrates/hybrid/per-surface-router.ts
39830
- function resolveHybridChoice(rules, surface) {
39831
- if (!rules) return null;
39832
- return rules.perSurface[surface] ?? null;
40870
+ // src/query-anonymity/header-strip.ts
40871
+ function stripHeaders(headers) {
40872
+ const stripped = {};
40873
+ const removed = [];
40874
+ for (const [name, value] of Object.entries(headers)) {
40875
+ const lower = name.toLowerCase();
40876
+ if (REQUIRED_HEADER_SET.has(lower)) {
40877
+ stripped[name] = value;
40878
+ continue;
40879
+ }
40880
+ const reason = STRIP_REASON_BY_NAME.get(lower);
40881
+ if (reason !== void 0) {
40882
+ removed.push({ name, reason });
40883
+ continue;
40884
+ }
40885
+ stripped[name] = value;
40886
+ }
40887
+ return { stripped, removed };
39833
40888
  }
39834
- function validateHybridRules(rules) {
39835
- const surfaces = [
39836
- "concierge",
39837
- "direct-agent-gate-advisor",
39838
- "sentinel-scoring",
39839
- "gate-explanation",
40889
+ function defeatUndiciDefaultsInto(headers) {
40890
+ if (headers["user-agent"] === void 0 && headers["User-Agent"] === void 0) {
40891
+ headers["User-Agent"] = "";
40892
+ }
40893
+ if (headers["accept-language"] === void 0 && headers["Accept-Language"] === void 0) {
40894
+ headers["Accept-Language"] = "";
40895
+ }
40896
+ return headers;
40897
+ }
40898
+ function createAnonymizedFetch(baseFetch, onAudit) {
40899
+ const wrapped = async (input, init) => {
40900
+ const headers = normalizeHeadersInit(init?.headers);
40901
+ const result = stripHeaders(headers);
40902
+ defeatUndiciDefaultsInto(result.stripped);
40903
+ const preservedRequired = Object.keys(result.stripped).filter(
40904
+ (k) => REQUIRED_HEADER_SET.has(k.toLowerCase())
40905
+ );
40906
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
40907
+ const method = init?.method ?? (input instanceof Request ? input.method : "GET");
40908
+ if (onAudit) {
40909
+ onAudit({
40910
+ url,
40911
+ method,
40912
+ stripped_count: result.removed.length,
40913
+ removed: result.removed,
40914
+ required_preserved: preservedRequired
40915
+ });
40916
+ }
40917
+ return baseFetch(input, { ...init, headers: result.stripped });
40918
+ };
40919
+ return wrapped;
40920
+ }
40921
+ function normalizeHeadersInit(raw) {
40922
+ if (raw === void 0) return {};
40923
+ if (typeof Headers !== "undefined" && raw instanceof Headers) {
40924
+ const out = {};
40925
+ raw.forEach((value, key) => {
40926
+ out[key] = value;
40927
+ });
40928
+ return out;
40929
+ }
40930
+ if (Array.isArray(raw)) {
40931
+ const out = {};
40932
+ for (const [k, v] of raw) {
40933
+ if (k !== void 0 && v !== void 0) out[k] = v;
40934
+ }
40935
+ return out;
40936
+ }
40937
+ return { ...raw };
40938
+ }
40939
+ var QUERY_ANONYMITY_AUDIT_OPS, CANONICAL_STRIP_LIST, REQUIRED_HEADERS, REQUIRED_HEADER_SET, STRIP_REASON_BY_NAME;
40940
+ var init_header_strip = __esm({
40941
+ "src/query-anonymity/header-strip.ts"() {
40942
+ QUERY_ANONYMITY_AUDIT_OPS = {
40943
+ HEADERS_STRIPPED: "query_anonymity_headers_stripped"
40944
+ };
40945
+ CANONICAL_STRIP_LIST = [
40946
+ // Browser / runtime fingerprinting.
40947
+ { name: "user-agent", reason: "user-agent" },
40948
+ { name: "sec-ch-ua", reason: "fingerprintable-extension" },
40949
+ { name: "sec-ch-ua-mobile", reason: "fingerprintable-extension" },
40950
+ { name: "sec-ch-ua-platform", reason: "fingerprintable-extension" },
40951
+ { name: "sec-ch-ua-platform-version", reason: "fingerprintable-extension" },
40952
+ { name: "sec-ch-ua-arch", reason: "fingerprintable-extension" },
40953
+ { name: "sec-ch-ua-bitness", reason: "fingerprintable-extension" },
40954
+ { name: "sec-ch-ua-model", reason: "fingerprintable-extension" },
40955
+ { name: "sec-ch-ua-full-version-list", reason: "fingerprintable-extension" },
40956
+ // Locale fingerprint.
40957
+ { name: "accept-language", reason: "locale-fingerprint" },
40958
+ // Request-origin leak.
40959
+ { name: "referer", reason: "leaking-network-info" },
40960
+ { name: "referrer-policy", reason: "leaking-network-info" },
40961
+ { name: "origin", reason: "leaking-network-info" },
40962
+ // Forwarded-by / IP-derived network info.
40963
+ { name: "via", reason: "leaking-network-info" },
40964
+ { name: "forwarded", reason: "leaking-network-info" },
40965
+ { name: "x-forwarded-for", reason: "leaking-network-info" },
40966
+ { name: "x-real-ip", reason: "leaking-network-info" },
40967
+ { name: "x-client-ip", reason: "leaking-network-info" },
40968
+ // DNT / GPC are technically anti-tracking signals but they
40969
+ // themselves form a fingerprint (operators who set DNT=1 are a
40970
+ // smaller subset). Strip to keep the substrate ignorant of
40971
+ // operator preferences.
40972
+ { name: "dnt", reason: "unnecessary-metadata" },
40973
+ { name: "sec-gpc", reason: "unnecessary-metadata" }
40974
+ ];
40975
+ REQUIRED_HEADERS = [
40976
+ "authorization",
40977
+ "content-type",
40978
+ "content-length",
40979
+ "host",
40980
+ "accept",
40981
+ "x-api-key",
40982
+ // Anthropic API auth
40983
+ "anthropic-version",
40984
+ // Anthropic API contract version
40985
+ "anthropic-beta",
40986
+ // optional Anthropic beta opt-in
40987
+ "openai-organization",
40988
+ // optional OpenAI org id
40989
+ "x-stainless-package-version",
40990
+ // allowed for Anthropic + OpenAI SDK contract compat
40991
+ "x-goog-api-key",
40992
+ // Google AI Studio
40993
+ "x-goog-user-project"
40994
+ // Google AI Studio
40995
+ ];
40996
+ REQUIRED_HEADER_SET = new Set(
40997
+ REQUIRED_HEADERS.map((h) => h.toLowerCase())
40998
+ );
40999
+ STRIP_REASON_BY_NAME = new Map(
41000
+ CANONICAL_STRIP_LIST.map((h) => [h.name.toLowerCase(), h.reason])
41001
+ );
41002
+ }
41003
+ });
41004
+
41005
+ // src/intelligence/substrates/hybrid/per-surface-router.ts
41006
+ function resolveHybridChoice(rules, surface) {
41007
+ if (!rules) return null;
41008
+ return rules.perSurface[surface] ?? null;
41009
+ }
41010
+ function validateHybridRules(rules) {
41011
+ const surfaces = [
41012
+ "concierge",
41013
+ "direct-agent-gate-advisor",
41014
+ "sentinel-scoring",
41015
+ "gate-explanation",
39840
41016
  "privacy-filter-tier-2",
39841
41017
  "template-suggestion"
39842
41018
  ];
@@ -39947,6 +41123,7 @@ var init_selector = __esm({
39947
41123
  init_local();
39948
41124
  init_venice();
39949
41125
  init_frontier();
41126
+ init_header_strip();
39950
41127
  init_per_surface_router();
39951
41128
  DISABLED_CAPABILITY = {
39952
41129
  summarize: false,
@@ -39981,7 +41158,21 @@ var init_selector = __esm({
39981
41158
  this.auditLog = cfg.auditLog;
39982
41159
  this.identityId = cfg.identityId;
39983
41160
  this.redactor = cfg.redactor ?? IDENTITY_REDACTOR;
39984
- this.fetchImpl = cfg.fetchImpl;
41161
+ const baseFetch = cfg.fetchImpl ?? globalThis.fetch;
41162
+ this.fetchImpl = createAnonymizedFetch(baseFetch, (event) => {
41163
+ this.auditLog.append(
41164
+ "l2",
41165
+ QUERY_ANONYMITY_AUDIT_OPS.HEADERS_STRIPPED,
41166
+ this.identityId,
41167
+ {
41168
+ url: event.url,
41169
+ method: event.method,
41170
+ stripped_count: event.stripped_count,
41171
+ removed: event.removed,
41172
+ required_preserved: event.required_preserved
41173
+ }
41174
+ );
41175
+ });
39985
41176
  this.config = buildDefaultConfig();
39986
41177
  }
39987
41178
  /**
@@ -40698,6 +41889,247 @@ var init_constants5 = __esm({
40698
41889
  ];
40699
41890
  }
40700
41891
  });
41892
+ async function issueDidWeb(opts) {
41893
+ if (!opts.authority_host || !HOST_RE.test(opts.authority_host)) {
41894
+ throw new Error(
41895
+ `did-web: authority_host '${opts.authority_host}' is not a valid DNS host`
41896
+ );
41897
+ }
41898
+ if (!FORTRESS_LABEL_RE.test(opts.fortress_id)) {
41899
+ throw new Error(
41900
+ `did-web: fortress_id '${opts.fortress_id}' is not a valid label`
41901
+ );
41902
+ }
41903
+ if (opts.agent_label !== void 0 && !AGENT_LABEL_RE.test(opts.agent_label)) {
41904
+ throw new Error(
41905
+ `did-web: agent_label '${opts.agent_label}' is not a valid label`
41906
+ );
41907
+ }
41908
+ if (opts.public_key.length !== 32) {
41909
+ throw new Error(
41910
+ `did-web: public_key must be exactly 32 bytes (Ed25519), got ${opts.public_key.length}`
41911
+ );
41912
+ }
41913
+ const did = buildDid(opts);
41914
+ const verificationMethodId = `${did}#key-1`;
41915
+ const verificationMethod = {
41916
+ id: verificationMethodId,
41917
+ type: "JsonWebKey2020",
41918
+ controller: did,
41919
+ publicKeyJwk: {
41920
+ kty: "OKP",
41921
+ crv: "Ed25519",
41922
+ x: toBase64url(opts.public_key)
41923
+ }
41924
+ };
41925
+ const didDocument = {
41926
+ "@context": [...DID_CONTEXT],
41927
+ id: did,
41928
+ verificationMethod: [verificationMethod],
41929
+ authentication: [verificationMethodId],
41930
+ assertionMethod: [verificationMethodId]
41931
+ };
41932
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
41933
+ return {
41934
+ did,
41935
+ did_document: didDocument,
41936
+ public_key: opts.public_key,
41937
+ created_at: now.toISOString(),
41938
+ authority_host: opts.authority_host,
41939
+ fortress_id: opts.fortress_id,
41940
+ ...opts.agent_label !== void 0 ? { agent_label: opts.agent_label } : {}
41941
+ };
41942
+ }
41943
+ function publishDidWebDocument(identifier, opts = {}) {
41944
+ const path = opts.publish_path ?? canonicalPublishPath(identifier);
41945
+ const artifact = canonicalSerializeDidDocument(identifier.did_document);
41946
+ const digest = sha256(stringToBytes(artifact));
41947
+ const url = `https://${identifier.authority_host}${path}`;
41948
+ return {
41949
+ url,
41950
+ publish_path: path,
41951
+ artifact,
41952
+ sha256: hashToString(digest)
41953
+ };
41954
+ }
41955
+ async function resolveDidWeb(did, opts) {
41956
+ const parsed = parseDidWeb(did);
41957
+ const url = didToUrl(parsed);
41958
+ if (!opts.allowed_hosts.includes(parsed.authority_host)) {
41959
+ return {
41960
+ ok: false,
41961
+ failure: "host_not_allowed",
41962
+ message: `did-web: authority_host '${parsed.authority_host}' is not in the operator's allowed_hosts allowlist; resolution refused (no-outbound-by-default)`,
41963
+ url
41964
+ };
41965
+ }
41966
+ const timeoutMs = opts.timeout_ms ?? DEFAULT_TIMEOUT_MS4;
41967
+ const fetcher = opts.fetcher ?? defaultFetcher;
41968
+ const controller = new AbortController();
41969
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
41970
+ let response;
41971
+ try {
41972
+ response = await fetcher(url, { signal: controller.signal });
41973
+ } catch (err) {
41974
+ clearTimeout(timer);
41975
+ const message = err instanceof Error ? err.message : String(err);
41976
+ if (controller.signal.aborted) {
41977
+ return {
41978
+ ok: false,
41979
+ failure: "timeout",
41980
+ message: `did-web: resolution exceeded ${timeoutMs}ms`,
41981
+ url
41982
+ };
41983
+ }
41984
+ return {
41985
+ ok: false,
41986
+ failure: "fetch_failed",
41987
+ message: `did-web: fetch error: ${message}`,
41988
+ url
41989
+ };
41990
+ }
41991
+ clearTimeout(timer);
41992
+ if (response.status === 404) {
41993
+ return {
41994
+ ok: false,
41995
+ failure: "not_found",
41996
+ message: `did-web: 404 from authority host`,
41997
+ url
41998
+ };
41999
+ }
42000
+ if (!response.ok) {
42001
+ return {
42002
+ ok: false,
42003
+ failure: "fetch_failed",
42004
+ message: `did-web: authority host returned ${response.status}`,
42005
+ url
42006
+ };
42007
+ }
42008
+ let body;
42009
+ try {
42010
+ body = await response.json();
42011
+ } catch (err) {
42012
+ const message = err instanceof Error ? err.message : String(err);
42013
+ return {
42014
+ ok: false,
42015
+ failure: "invalid_json",
42016
+ message: `did-web: invalid JSON: ${message}`,
42017
+ url
42018
+ };
42019
+ }
42020
+ if (!isDidDocument(body, did)) {
42021
+ return {
42022
+ ok: false,
42023
+ failure: "invalid_json",
42024
+ message: `did-web: response body is not a valid DID Document for ${did}`,
42025
+ url
42026
+ };
42027
+ }
42028
+ if (opts.expected_public_key !== void 0) {
42029
+ const expectedX = toBase64url(opts.expected_public_key);
42030
+ const actualX = body.verificationMethod[0]?.publicKeyJwk.x;
42031
+ if (actualX !== expectedX) {
42032
+ return {
42033
+ ok: false,
42034
+ failure: "signature_mismatch",
42035
+ message: `did-web: verificationMethod public key does not match expected key`,
42036
+ url
42037
+ };
42038
+ }
42039
+ }
42040
+ return { ok: true, did_document: body, url };
42041
+ }
42042
+ function parseDidWeb(did) {
42043
+ if (!did.startsWith("did:web:")) {
42044
+ throw new Error(`did-web: '${did}' is not a did:web identifier`);
42045
+ }
42046
+ const rest = did.slice("did:web:".length);
42047
+ const segments = rest.split(":");
42048
+ const authorityHost = segments[0];
42049
+ if (!HOST_RE.test(authorityHost)) {
42050
+ throw new Error(`did-web: '${authorityHost}' is not a valid DNS host`);
42051
+ }
42052
+ const parsed = { authority_host: authorityHost };
42053
+ if (segments.length === 1) return parsed;
42054
+ if (segments.length === 5 && segments[1] === "fortress" && segments[3] === "agent") {
42055
+ parsed.fortress_id = segments[2];
42056
+ parsed.agent_label = segments[4];
42057
+ return parsed;
42058
+ }
42059
+ throw new Error(
42060
+ `did-web: '${did}' does not match the supported shapes (bare did:web:<host> or did:web:<host>:fortress:<fid>:agent:<alabel>)`
42061
+ );
42062
+ }
42063
+ function didToUrl(parsed) {
42064
+ if (parsed.fortress_id === void 0 || parsed.agent_label === void 0) {
42065
+ return `https://${parsed.authority_host}/.well-known/did.json`;
42066
+ }
42067
+ return `https://${parsed.authority_host}/fortress/${parsed.fortress_id}/agent/${parsed.agent_label}/did.json`;
42068
+ }
42069
+ function buildDid(opts) {
42070
+ if (opts.agent_label === void 0) {
42071
+ return `did:web:${opts.authority_host}`;
42072
+ }
42073
+ return `did:web:${opts.authority_host}:fortress:${opts.fortress_id}:agent:${opts.agent_label}`;
42074
+ }
42075
+ function canonicalPublishPath(identifier) {
42076
+ if (identifier.agent_label === void 0) {
42077
+ return "/.well-known/did.json";
42078
+ }
42079
+ return `/fortress/${identifier.fortress_id}/agent/${identifier.agent_label}/did.json`;
42080
+ }
42081
+ function canonicalSerializeDidDocument(doc) {
42082
+ return JSON.stringify(
42083
+ {
42084
+ "@context": doc["@context"],
42085
+ id: doc.id,
42086
+ verificationMethod: doc.verificationMethod,
42087
+ authentication: doc.authentication,
42088
+ assertionMethod: doc.assertionMethod
42089
+ },
42090
+ null,
42091
+ 2
42092
+ );
42093
+ }
42094
+ function isDidDocument(value, expectedDid) {
42095
+ if (!value || typeof value !== "object") return false;
42096
+ const v = value;
42097
+ if (v["id"] !== expectedDid) return false;
42098
+ if (!Array.isArray(v["@context"])) return false;
42099
+ const vm = v["verificationMethod"];
42100
+ if (!Array.isArray(vm) || vm.length === 0) return false;
42101
+ const first = vm[0];
42102
+ if (!first || typeof first["id"] !== "string") return false;
42103
+ const jwk = first["publicKeyJwk"];
42104
+ if (!jwk || jwk["kty"] !== "OKP" || jwk["crv"] !== "Ed25519") return false;
42105
+ if (typeof jwk["x"] !== "string") return false;
42106
+ if (!Array.isArray(v["authentication"])) return false;
42107
+ if (!Array.isArray(v["assertionMethod"])) return false;
42108
+ return true;
42109
+ }
42110
+ async function defaultFetcher(url, init) {
42111
+ const response = await fetch(url, init);
42112
+ return {
42113
+ ok: response.ok,
42114
+ status: response.status,
42115
+ json: () => response.json()
42116
+ };
42117
+ }
42118
+ var DID_CONTEXT, DEFAULT_TIMEOUT_MS4, HOST_RE, FORTRESS_LABEL_RE, AGENT_LABEL_RE;
42119
+ var init_did_web = __esm({
42120
+ "src/recognition/did-web.ts"() {
42121
+ init_encoding();
42122
+ init_hashing();
42123
+ DID_CONTEXT = [
42124
+ "https://www.w3.org/ns/did/v1",
42125
+ "https://w3id.org/security/suites/jws-2020/v1"
42126
+ ];
42127
+ DEFAULT_TIMEOUT_MS4 = 5e3;
42128
+ HOST_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
42129
+ FORTRESS_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
42130
+ AGENT_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
42131
+ }
42132
+ });
40701
42133
 
40702
42134
  // src/contracts/v1.1/exit-bundle-manifest.ts
40703
42135
  var EXIT_BUNDLE_PATH_PATTERN, EXIT_BUNDLE_PATH_MAX_BYTES;
@@ -41401,6 +42833,7 @@ async function exportExitBundle(opts) {
41401
42833
  "placeholder_vault_metadata"
41402
42834
  )
41403
42835
  );
42836
+ const didWebBinding = validateExportDidWeb(opts.didWeb);
41404
42837
  const body = {
41405
42838
  manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
41406
42839
  exported_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -41408,7 +42841,8 @@ async function exportExitBundle(opts) {
41408
42841
  identity_id: identity.identity_id,
41409
42842
  fortress_id: identity.did,
41410
42843
  fortress_master_pubkey: identity.public_key,
41411
- did: identity.did
42844
+ did: identity.did,
42845
+ ...didWebBinding !== void 0 ? { did_web: didWebBinding } : {}
41412
42846
  },
41413
42847
  source_sanctuary_version: opts.config?.version ?? SANCTUARY_VERSION,
41414
42848
  artifacts,
@@ -41430,6 +42864,18 @@ async function exportExitBundle(opts) {
41430
42864
  };
41431
42865
  const manifestBytes = jsonBytes(manifest);
41432
42866
  await writeFile(join(bundleDir, "manifest.json"), manifestBytes, { mode: 384 });
42867
+ if (didWebBinding !== void 0) {
42868
+ opts.auditLog.append(
42869
+ "l1",
42870
+ EXIT_BUNDLE_DID_WEB_AUDIT_OPS.EXPORT_INCLUDED,
42871
+ identity.identity_id,
42872
+ {
42873
+ approval_id: exportApprovalAuditId,
42874
+ identifier: didWebBinding.identifier,
42875
+ authority_host: didWebBinding.authority_host
42876
+ }
42877
+ );
42878
+ }
41433
42879
  await opts.auditLog.flush();
41434
42880
  return {
41435
42881
  bundle_dir: bundleDir,
@@ -41441,6 +42887,30 @@ async function exportExitBundle(opts) {
41441
42887
  ]
41442
42888
  };
41443
42889
  }
42890
+ function validateExportDidWeb(binding) {
42891
+ if (binding === void 0) return void 0;
42892
+ if (!binding.identifier || typeof binding.identifier !== "string") {
42893
+ throw new Error(
42894
+ "exit-bundle: did_web.identifier must be a non-empty did:web URI"
42895
+ );
42896
+ }
42897
+ if (!binding.authority_host || typeof binding.authority_host !== "string") {
42898
+ throw new Error(
42899
+ "exit-bundle: did_web.authority_host must be a non-empty DNS host"
42900
+ );
42901
+ }
42902
+ const parsed = parseDidWeb(binding.identifier);
42903
+ if (parsed.authority_host.toLowerCase() !== binding.authority_host.toLowerCase()) {
42904
+ throw new Error(
42905
+ `exit-bundle: did_web.identifier authority host '${parsed.authority_host}' does not match did_web.authority_host '${binding.authority_host}'`
42906
+ );
42907
+ }
42908
+ return {
42909
+ identifier: binding.identifier,
42910
+ authority_host: binding.authority_host,
42911
+ ...binding.published_at !== void 0 ? { published_at: binding.published_at } : {}
42912
+ };
42913
+ }
41444
42914
  function publicKeysFromIdentityArtifact(identityArtifact) {
41445
42915
  const pubkey = fromBase64url(identityArtifact.bundle.publicKey);
41446
42916
  return {
@@ -41655,6 +43125,87 @@ async function importExitBundle(opts) {
41655
43125
  };
41656
43126
  }
41657
43127
  const manifest = await readManifest(opts.bundleDir);
43128
+ const importWarnings = [];
43129
+ const manifestDidWeb = manifest.body.identity_binding.did_web;
43130
+ if (manifestDidWeb !== void 0 && !opts.skipDidWebVerify) {
43131
+ const expectedPublicKey = fromBase64url(
43132
+ manifest.body.identity_binding.fortress_master_pubkey
43133
+ );
43134
+ const resolveOpts = {
43135
+ allowed_hosts: opts.didWebAllowedHosts ?? [],
43136
+ expected_public_key: expectedPublicKey,
43137
+ ...opts.didWebFetcher !== void 0 ? { fetcher: opts.didWebFetcher } : {},
43138
+ ...opts.didWebTimeoutMs !== void 0 ? { timeout_ms: opts.didWebTimeoutMs } : {}
43139
+ };
43140
+ const resolution = await resolveDidWeb(
43141
+ manifestDidWeb.identifier,
43142
+ resolveOpts
43143
+ );
43144
+ const authorityHost = manifestDidWeb.authority_host;
43145
+ opts.auditLog.append(
43146
+ "l1",
43147
+ EXIT_BUNDLE_DID_WEB_AUDIT_OPS.AUTHORITY_HOST,
43148
+ manifest.body.identity_binding.identity_id,
43149
+ { authority_host: authorityHost, identifier: manifestDidWeb.identifier }
43150
+ );
43151
+ if (resolution.ok) {
43152
+ opts.auditLog.append(
43153
+ "l1",
43154
+ EXIT_BUNDLE_DID_WEB_AUDIT_OPS.IMPORT_VERIFIED,
43155
+ manifest.body.identity_binding.identity_id,
43156
+ {
43157
+ outcome: "success",
43158
+ identifier: manifestDidWeb.identifier,
43159
+ authority_host: authorityHost,
43160
+ resolved_url: resolution.url
43161
+ }
43162
+ );
43163
+ } else if (resolution.failure === "signature_mismatch") {
43164
+ opts.auditLog.append(
43165
+ "l1",
43166
+ EXIT_BUNDLE_DID_WEB_AUDIT_OPS.IMPORT_VERIFIED,
43167
+ manifest.body.identity_binding.identity_id,
43168
+ {
43169
+ outcome: "mismatch",
43170
+ identifier: manifestDidWeb.identifier,
43171
+ authority_host: authorityHost,
43172
+ resolved_url: resolution.url
43173
+ }
43174
+ );
43175
+ await opts.auditLog.flush();
43176
+ throw new ExitBundleImportError(
43177
+ "did_web_mismatch",
43178
+ `did:web cross-check failed: the DID Document at ${resolution.url} resolved successfully, but the verificationMethod public key did not match the manifest's claimed fortress_master_pubkey. The bundle's claimed origin (${manifestDidWeb.identifier}) is inconsistent with the published DID Document. To proceed anyway with the manifest signature alone, re-run import with --skip-did-web-verify.`
43179
+ );
43180
+ } else {
43181
+ opts.auditLog.append(
43182
+ "l1",
43183
+ EXIT_BUNDLE_DID_WEB_AUDIT_OPS.IMPORT_VERIFIED,
43184
+ manifest.body.identity_binding.identity_id,
43185
+ {
43186
+ outcome: "resolution_failure",
43187
+ failure: resolution.failure,
43188
+ identifier: manifestDidWeb.identifier,
43189
+ authority_host: authorityHost,
43190
+ resolved_url: resolution.url
43191
+ }
43192
+ );
43193
+ importWarnings.push(
43194
+ `did:web resolution failed (${resolution.failure}): ${resolution.message}. Import proceeded with manifest-signature verification alone; recognition-layer cross-check was skipped. Re-run with --did-web-allowed-host=<host> to enable resolution, or --skip-did-web-verify to skip deliberately.`
43195
+ );
43196
+ }
43197
+ } else if (manifestDidWeb !== void 0 && opts.skipDidWebVerify) {
43198
+ opts.auditLog.append(
43199
+ "l1",
43200
+ EXIT_BUNDLE_DID_WEB_AUDIT_OPS.IMPORT_VERIFIED,
43201
+ manifest.body.identity_binding.identity_id,
43202
+ {
43203
+ outcome: "skipped",
43204
+ identifier: manifestDidWeb.identifier,
43205
+ authority_host: manifestDidWeb.authority_host
43206
+ }
43207
+ );
43208
+ }
41658
43209
  const identityArtifact = await loadExitArtifact(
41659
43210
  opts.bundleDir,
41660
43211
  manifest,
@@ -41719,7 +43270,7 @@ async function importExitBundle(opts) {
41719
43270
  unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
41720
43271
  },
41721
43272
  staged_artifacts: [],
41722
- warnings: verification.warnings,
43273
+ warnings: [...verification.warnings, ...importWarnings],
41723
43274
  unsupported_artifacts: verification.unsupported_artifacts
41724
43275
  };
41725
43276
  }
@@ -41886,7 +43437,7 @@ async function importExitBundle(opts) {
41886
43437
  state: stateResult,
41887
43438
  reputation: reputationResult,
41888
43439
  staged_artifacts: stagedArtifacts,
41889
- warnings: verification.warnings,
43440
+ warnings: [...verification.warnings, ...importWarnings],
41890
43441
  unsupported_artifacts: verification.unsupported_artifacts
41891
43442
  };
41892
43443
  }
@@ -41908,12 +43459,13 @@ function exitBundleManifestShape() {
41908
43459
  ]
41909
43460
  };
41910
43461
  }
41911
- var ARTIFACT_DIR, EXIT_IMPORT_NAMESPACE, EXIT_PUBLIC_IDENTITIES_NAMESPACE, EXIT_AUDIT_RECEIPTS_NAMESPACE, EXIT_POLICY_SETS_NAMESPACE, EXIT_COMMITMENTS_NAMESPACE, EXIT_PLACEHOLDER_METADATA_NAMESPACE, PRIVACY_PLACEHOLDER_NAMESPACE, ExitBundleImportError;
43462
+ var ARTIFACT_DIR, EXIT_BUNDLE_DID_WEB_AUDIT_OPS, EXIT_IMPORT_NAMESPACE, EXIT_PUBLIC_IDENTITIES_NAMESPACE, EXIT_AUDIT_RECEIPTS_NAMESPACE, EXIT_POLICY_SETS_NAMESPACE, EXIT_COMMITMENTS_NAMESPACE, EXIT_PLACEHOLDER_METADATA_NAMESPACE, PRIVACY_PLACEHOLDER_NAMESPACE, ExitBundleImportError;
41912
43463
  var init_bundle = __esm({
41913
43464
  "src/exit/bundle.ts"() {
41914
43465
  init_state_store();
41915
43466
  init_config();
41916
43467
  init_constants5();
43468
+ init_did_web();
41917
43469
  init_canonical_json();
41918
43470
  init_hashing();
41919
43471
  init_encoding();
@@ -41923,6 +43475,11 @@ var init_bundle = __esm({
41923
43475
  init_reputation_store();
41924
43476
  init_verifier2();
41925
43477
  ARTIFACT_DIR = "artifacts";
43478
+ EXIT_BUNDLE_DID_WEB_AUDIT_OPS = {
43479
+ EXPORT_INCLUDED: "exit_bundle_did_web_export_included",
43480
+ IMPORT_VERIFIED: "exit_bundle_did_web_import_verified",
43481
+ AUTHORITY_HOST: "exit_bundle_did_web_authority_host"
43482
+ };
41926
43483
  EXIT_IMPORT_NAMESPACE = "_exit_imports";
41927
43484
  EXIT_PUBLIC_IDENTITIES_NAMESPACE = "_exit_public_identities";
41928
43485
  EXIT_AUDIT_RECEIPTS_NAMESPACE = "_exit_audit_receipts";
@@ -42141,6 +43698,26 @@ ${policyErr.message}
42141
43698
  }
42142
43699
  throw policyErr;
42143
43700
  }
43701
+ const includeDidWebFlag = flagValue(argv, "--include-did-web");
43702
+ const includeDidWebDisabled = includeDidWebFlag === "false";
43703
+ const didWebIdentifier = flagValue(argv, "--did-web");
43704
+ const didWebAuthorityHost = flagValue(argv, "--did-web-authority-host");
43705
+ const didWebPublishedAt = flagValue(argv, "--did-web-published-at");
43706
+ let exportDidWeb;
43707
+ if (!includeDidWebDisabled && didWebIdentifier !== void 0) {
43708
+ if (didWebAuthorityHost === void 0) {
43709
+ write(
43710
+ err,
43711
+ "Error: --did-web requires --did-web-authority-host=<host>\n"
43712
+ );
43713
+ return 2;
43714
+ }
43715
+ exportDidWeb = {
43716
+ identifier: didWebIdentifier,
43717
+ authority_host: didWebAuthorityHost,
43718
+ ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
43719
+ };
43720
+ }
42144
43721
  const result = await exportExitBundle({
42145
43722
  bundleDir: outDir,
42146
43723
  storage: ctx.storage,
@@ -42152,7 +43729,8 @@ ${policyErr.message}
42152
43729
  config,
42153
43730
  stateStoragePath: ctx.stateStoragePath,
42154
43731
  stateNamespaces: repeatedFlagValues(argv, "--state-namespace"),
42155
- keySource: ctx.keySource
43732
+ keySource: ctx.keySource,
43733
+ ...exportDidWeb !== void 0 ? { didWeb: exportDidWeb } : {}
42156
43734
  });
42157
43735
  if (json) write(out, JSON.stringify(result, null, 2) + "\n");
42158
43736
  else {
@@ -42230,6 +43808,11 @@ ${policyErr.message}
42230
43808
  write(err, "--conflict must be skip, overwrite, or version\n");
42231
43809
  return 2;
42232
43810
  }
43811
+ const didWebAllowedHosts = repeatedFlagValues(
43812
+ argv,
43813
+ "--did-web-allowed-host"
43814
+ );
43815
+ const skipDidWebVerify = hasFlag(argv, "--skip-did-web-verify");
42233
43816
  let result;
42234
43817
  try {
42235
43818
  result = await importExitBundle({
@@ -42245,7 +43828,9 @@ ${policyErr.message}
42245
43828
  conflictResolution: conflict,
42246
43829
  sourcePassphrase: flagValue(argv, "--source-passphrase"),
42247
43830
  sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
42248
- destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
43831
+ destinationSignerIdentityId: flagValue(argv, "--destination-identity-id"),
43832
+ ...didWebAllowedHosts.length > 0 ? { didWebAllowedHosts } : {},
43833
+ skipDidWebVerify
42249
43834
  });
42250
43835
  } catch (e) {
42251
43836
  if (e instanceof InvalidExitBundleError) {
@@ -43030,12 +44615,14 @@ ${err.message}
43030
44615
  fortressId: fortressIdForAggregator
43031
44616
  });
43032
44617
  const handoffEventBridge = new HandoffEventBridge();
44618
+ const workflowStateTracker = new WorkflowStateTracker();
43033
44619
  if (dashboard) {
43034
44620
  dashboard.setHandoffLog({
43035
44621
  handoffLog,
43036
44622
  eventBridge: handoffEventBridge,
43037
44623
  auditLog,
43038
- operatorId: aggregatorIdentityId
44624
+ operatorId: aggregatorIdentityId,
44625
+ workflowStateTracker
43039
44626
  });
43040
44627
  }
43041
44628
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
@@ -43237,6 +44824,7 @@ var init_src = __esm({
43237
44824
  init_anomaly_pipeline();
43238
44825
  init_handoff_log();
43239
44826
  init_handoff_routes();
44827
+ init_workflow_state_tracker();
43240
44828
  init_sentinels();
43241
44829
  init_subscription_store();
43242
44830
  init_tools4();
@@ -47343,7 +48931,7 @@ async function probeTenantDashboard(tenant, options = {}) {
47343
48931
  if (!rt) {
47344
48932
  return { running: false, status: null, reason: "no runtime.json" };
47345
48933
  }
47346
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
48934
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS5;
47347
48935
  return await new Promise((resolve8) => {
47348
48936
  const req = get$1(
47349
48937
  {
@@ -47379,10 +48967,10 @@ async function probeTenantDashboard(tenant, options = {}) {
47379
48967
  });
47380
48968
  });
47381
48969
  }
47382
- var DEFAULT_TIMEOUT_MS4;
48970
+ var DEFAULT_TIMEOUT_MS5;
47383
48971
  var init_health = __esm({
47384
48972
  "src/cli/agents/health.ts"() {
47385
- DEFAULT_TIMEOUT_MS4 = 500;
48973
+ DEFAULT_TIMEOUT_MS5 = 500;
47386
48974
  }
47387
48975
  });
47388
48976
  function resolveCtx(args) {
@@ -48633,108 +50221,6 @@ var init_sentinel2 = __esm({
48633
50221
  init_sentinels();
48634
50222
  }
48635
50223
  });
48636
- async function issueDidWeb(opts) {
48637
- if (!opts.authority_host || !HOST_RE.test(opts.authority_host)) {
48638
- throw new Error(
48639
- `did-web: authority_host '${opts.authority_host}' is not a valid DNS host`
48640
- );
48641
- }
48642
- if (!FORTRESS_LABEL_RE.test(opts.fortress_id)) {
48643
- throw new Error(
48644
- `did-web: fortress_id '${opts.fortress_id}' is not a valid label`
48645
- );
48646
- }
48647
- if (opts.agent_label !== void 0 && !AGENT_LABEL_RE.test(opts.agent_label)) {
48648
- throw new Error(
48649
- `did-web: agent_label '${opts.agent_label}' is not a valid label`
48650
- );
48651
- }
48652
- if (opts.public_key.length !== 32) {
48653
- throw new Error(
48654
- `did-web: public_key must be exactly 32 bytes (Ed25519), got ${opts.public_key.length}`
48655
- );
48656
- }
48657
- const did = buildDid(opts);
48658
- const verificationMethodId = `${did}#key-1`;
48659
- const verificationMethod = {
48660
- id: verificationMethodId,
48661
- type: "JsonWebKey2020",
48662
- controller: did,
48663
- publicKeyJwk: {
48664
- kty: "OKP",
48665
- crv: "Ed25519",
48666
- x: toBase64url(opts.public_key)
48667
- }
48668
- };
48669
- const didDocument = {
48670
- "@context": [...DID_CONTEXT],
48671
- id: did,
48672
- verificationMethod: [verificationMethod],
48673
- authentication: [verificationMethodId],
48674
- assertionMethod: [verificationMethodId]
48675
- };
48676
- const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
48677
- return {
48678
- did,
48679
- did_document: didDocument,
48680
- public_key: opts.public_key,
48681
- created_at: now.toISOString(),
48682
- authority_host: opts.authority_host,
48683
- fortress_id: opts.fortress_id,
48684
- ...opts.agent_label !== void 0 ? { agent_label: opts.agent_label } : {}
48685
- };
48686
- }
48687
- function publishDidWebDocument(identifier, opts = {}) {
48688
- const path = opts.publish_path ?? canonicalPublishPath(identifier);
48689
- const artifact = canonicalSerializeDidDocument(identifier.did_document);
48690
- const digest = sha256(stringToBytes(artifact));
48691
- const url = `https://${identifier.authority_host}${path}`;
48692
- return {
48693
- url,
48694
- publish_path: path,
48695
- artifact,
48696
- sha256: hashToString(digest)
48697
- };
48698
- }
48699
- function buildDid(opts) {
48700
- if (opts.agent_label === void 0) {
48701
- return `did:web:${opts.authority_host}`;
48702
- }
48703
- return `did:web:${opts.authority_host}:fortress:${opts.fortress_id}:agent:${opts.agent_label}`;
48704
- }
48705
- function canonicalPublishPath(identifier) {
48706
- if (identifier.agent_label === void 0) {
48707
- return "/.well-known/did.json";
48708
- }
48709
- return `/fortress/${identifier.fortress_id}/agent/${identifier.agent_label}/did.json`;
48710
- }
48711
- function canonicalSerializeDidDocument(doc) {
48712
- return JSON.stringify(
48713
- {
48714
- "@context": doc["@context"],
48715
- id: doc.id,
48716
- verificationMethod: doc.verificationMethod,
48717
- authentication: doc.authentication,
48718
- assertionMethod: doc.assertionMethod
48719
- },
48720
- null,
48721
- 2
48722
- );
48723
- }
48724
- var DID_CONTEXT, HOST_RE, FORTRESS_LABEL_RE, AGENT_LABEL_RE;
48725
- var init_did_web = __esm({
48726
- "src/recognition/did-web.ts"() {
48727
- init_encoding();
48728
- init_hashing();
48729
- DID_CONTEXT = [
48730
- "https://www.w3.org/ns/did/v1",
48731
- "https://w3id.org/security/suites/jws-2020/v1"
48732
- ];
48733
- HOST_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
48734
- FORTRESS_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
48735
- AGENT_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
48736
- }
48737
- });
48738
50224
 
48739
50225
  // src/cli/did-web.ts
48740
50226
  var did_web_exports = {};
@@ -48993,6 +50479,689 @@ var init_did_web2 = __esm({
48993
50479
  }
48994
50480
  });
48995
50481
 
50482
+ // src/anomaly-detection/classifiers/rolling-baseline.ts
50483
+ var ROLLING_BASELINE_CLASSIFIER_ID, DEFAULT_MIN_SAMPLES_FOR_PREDICTION, STDDEV_FLOOR, RollingBaselineClassifier;
50484
+ var init_rolling_baseline = __esm({
50485
+ "src/anomaly-detection/classifiers/rolling-baseline.ts"() {
50486
+ ROLLING_BASELINE_CLASSIFIER_ID = "rolling-baseline";
50487
+ DEFAULT_MIN_SAMPLES_FOR_PREDICTION = 7;
50488
+ STDDEV_FLOOR = 0.5;
50489
+ RollingBaselineClassifier = class {
50490
+ classifierId = ROLLING_BASELINE_CLASSIFIER_ID;
50491
+ stateStore;
50492
+ minSamplesForPrediction;
50493
+ /** In-memory cache of per-agent state; loaded lazily on first touch. */
50494
+ cache = /* @__PURE__ */ new Map();
50495
+ /** Agents whose in-memory state has been mutated since last train(). */
50496
+ dirty = /* @__PURE__ */ new Set();
50497
+ constructor(opts) {
50498
+ this.stateStore = opts.stateStore;
50499
+ this.minSamplesForPrediction = opts.minSamplesForPrediction ?? DEFAULT_MIN_SAMPLES_FOR_PREDICTION;
50500
+ }
50501
+ async observe(vector) {
50502
+ const state = await this.loadOrInit(vector.agent_id);
50503
+ for (const [featureName, observed] of Object.entries(vector.features)) {
50504
+ if (!Number.isFinite(observed)) continue;
50505
+ const welford = state.features[featureName] ?? {
50506
+ n: 0,
50507
+ mean: 0,
50508
+ m2: 0
50509
+ };
50510
+ const nextN = welford.n + 1;
50511
+ const delta = observed - welford.mean;
50512
+ const nextMean = welford.mean + delta / nextN;
50513
+ const delta2 = observed - nextMean;
50514
+ const nextM2 = welford.m2 + delta * delta2;
50515
+ state.features[featureName] = { n: nextN, mean: nextMean, m2: nextM2 };
50516
+ }
50517
+ state.observation_count += 1;
50518
+ state.last_observed_at = vector.observed_at;
50519
+ this.dirty.add(vector.agent_id);
50520
+ }
50521
+ async predict(vector) {
50522
+ const state = await this.loadOrInit(vector.agent_id);
50523
+ if (state.observation_count < this.minSamplesForPrediction) {
50524
+ return {
50525
+ anomaly_score: 0,
50526
+ explanation: [],
50527
+ feature_contributions: [],
50528
+ baseline_ready: false
50529
+ };
50530
+ }
50531
+ const contributions = [];
50532
+ let sumSquaredZ = 0;
50533
+ for (const [featureName, observed] of Object.entries(vector.features)) {
50534
+ if (!Number.isFinite(observed)) continue;
50535
+ const welford = state.features[featureName];
50536
+ if (!welford || welford.n < 2) continue;
50537
+ const variance = welford.m2 / (welford.n - 1);
50538
+ const stddev = Math.max(Math.sqrt(variance), STDDEV_FLOOR);
50539
+ const z = (observed - welford.mean) / stddev;
50540
+ sumSquaredZ += z * z;
50541
+ contributions.push({
50542
+ feature_name: featureName,
50543
+ observed,
50544
+ baseline_mean: welford.mean,
50545
+ baseline_stddev: stddev,
50546
+ z_score: z
50547
+ });
50548
+ }
50549
+ contributions.sort(
50550
+ (a, b) => Math.abs(b.z_score) - Math.abs(a.z_score)
50551
+ );
50552
+ const anomalyScore = Math.sqrt(sumSquaredZ);
50553
+ const explanation = contributions.map(
50554
+ (c) => `${c.feature_name} ${c.observed.toFixed(2)} vs baseline ${c.baseline_mean.toFixed(2)}+/-${c.baseline_stddev.toFixed(2)} (z=${c.z_score.toFixed(2)})`
50555
+ );
50556
+ return {
50557
+ anomaly_score: anomalyScore,
50558
+ explanation,
50559
+ feature_contributions: contributions,
50560
+ baseline_ready: true
50561
+ };
50562
+ }
50563
+ async train() {
50564
+ const dirtyAgents = [...this.dirty];
50565
+ for (const agentId of dirtyAgents) {
50566
+ const state = this.cache.get(agentId);
50567
+ if (!state) continue;
50568
+ await this.stateStore.saveState(this.classifierId, agentId, state);
50569
+ this.dirty.delete(agentId);
50570
+ }
50571
+ let sampleCount = 0;
50572
+ for (const state of this.cache.values()) {
50573
+ sampleCount += state.observation_count;
50574
+ }
50575
+ return {
50576
+ trained_at: (/* @__PURE__ */ new Date()).toISOString(),
50577
+ sample_count: sampleCount,
50578
+ agent_count: this.cache.size
50579
+ };
50580
+ }
50581
+ /** Test helper: read the in-memory state for an agent. */
50582
+ getAgentState(agentId) {
50583
+ return this.cache.get(agentId);
50584
+ }
50585
+ async loadOrInit(agentId) {
50586
+ const cached = this.cache.get(agentId);
50587
+ if (cached) return cached;
50588
+ const persisted = await this.stateStore.loadState(
50589
+ this.classifierId,
50590
+ agentId
50591
+ );
50592
+ const state = persisted ?? {
50593
+ observation_count: 0,
50594
+ last_observed_at: null,
50595
+ features: {}
50596
+ };
50597
+ this.cache.set(agentId, state);
50598
+ return state;
50599
+ }
50600
+ };
50601
+ }
50602
+ });
50603
+
50604
+ // src/anomaly-detection/feature-extractors/per-agent-activity.ts
50605
+ function emptyBucket() {
50606
+ return {
50607
+ tool_call_count: 0,
50608
+ egress_call_count: 0,
50609
+ credential_use_count: 0,
50610
+ audit_event_count: 0,
50611
+ recent_receipt_count: 0
50612
+ };
50613
+ }
50614
+ function bucketToFeatures(bucket) {
50615
+ return {
50616
+ tool_call_count: bucket.tool_call_count,
50617
+ egress_call_count: bucket.egress_call_count,
50618
+ credential_use_count: bucket.credential_use_count,
50619
+ audit_event_count: bucket.audit_event_count,
50620
+ recent_receipt_count: bucket.recent_receipt_count
50621
+ };
50622
+ }
50623
+ function classifyEntry(entry, bucket) {
50624
+ bucket.audit_event_count += 1;
50625
+ bucket.tool_call_count += 1;
50626
+ if (entry.operation.startsWith("proxy_call:")) {
50627
+ bucket.egress_call_count += 1;
50628
+ }
50629
+ if (entry.operation.startsWith("broker_secret_") || entry.operation.startsWith("broker_token_")) {
50630
+ bucket.credential_use_count += 1;
50631
+ }
50632
+ if (entry.operation.startsWith("composition_receipt_") || entry.operation === "reputation_record" || entry.operation === "reputation_query" || entry.operation === "reputation_publish") {
50633
+ bucket.recent_receipt_count += 1;
50634
+ }
50635
+ }
50636
+ async function extractPerAgentActivity(context) {
50637
+ const now = context.now();
50638
+ const sinceIso = new Date(now.getTime() - WINDOW_MS2).toISOString();
50639
+ const result = await context.auditLog.query({
50640
+ since: sinceIso,
50641
+ limit: QUERY_LIMIT6
50642
+ });
50643
+ const buckets = /* @__PURE__ */ new Map();
50644
+ for (const entry of result.entries) {
50645
+ const agentId = entry.identity_id && entry.identity_id.length > 0 ? entry.identity_id : SYSTEM_AGENT_BUCKET;
50646
+ let bucket = buckets.get(agentId);
50647
+ if (!bucket) {
50648
+ bucket = emptyBucket();
50649
+ buckets.set(agentId, bucket);
50650
+ }
50651
+ classifyEntry(entry, bucket);
50652
+ }
50653
+ const observedAt = now.toISOString();
50654
+ const vectors = [];
50655
+ for (const [agentId, bucket] of buckets.entries()) {
50656
+ vectors.push({
50657
+ agent_id: agentId,
50658
+ observed_at: observedAt,
50659
+ features: bucketToFeatures(bucket),
50660
+ window_label: PER_AGENT_ACTIVITY_WINDOW_LABEL
50661
+ });
50662
+ }
50663
+ vectors.sort((a, b) => a.agent_id < b.agent_id ? -1 : 1);
50664
+ return vectors;
50665
+ }
50666
+ var PER_AGENT_ACTIVITY_EXTRACTOR_ID, WINDOW_MS2, QUERY_LIMIT6, SYSTEM_AGENT_BUCKET, PER_AGENT_ACTIVITY_WINDOW_LABEL;
50667
+ var init_per_agent_activity = __esm({
50668
+ "src/anomaly-detection/feature-extractors/per-agent-activity.ts"() {
50669
+ PER_AGENT_ACTIVITY_EXTRACTOR_ID = "per-agent-activity";
50670
+ WINDOW_MS2 = 24 * 60 * 60 * 1e3;
50671
+ QUERY_LIMIT6 = 1e4;
50672
+ SYSTEM_AGENT_BUCKET = "system";
50673
+ PER_AGENT_ACTIVITY_WINDOW_LABEL = "24h_rolling";
50674
+ }
50675
+ });
50676
+
50677
+ // src/anomaly-detection/detectors/per-agent-activity-detector.ts
50678
+ var PER_AGENT_ACTIVITY_DETECTOR_ID, PerAgentActivityDetector, PendingClassifier;
50679
+ var init_per_agent_activity_detector = __esm({
50680
+ "src/anomaly-detection/detectors/per-agent-activity-detector.ts"() {
50681
+ init_types4();
50682
+ init_rolling_baseline();
50683
+ init_classifier_state_store();
50684
+ init_per_agent_activity();
50685
+ PER_AGENT_ACTIVITY_DETECTOR_ID = PER_AGENT_ACTIVITY_EXTRACTOR_ID;
50686
+ PerAgentActivityDetector = class extends AnomalyDetector {
50687
+ detectorId = PER_AGENT_ACTIVITY_DETECTOR_ID;
50688
+ description = "Per-agent statistical drift detector: tool-call count, egress volume, credential-use rate, audit-event count, recent-receipt count over a 24h rolling window. Compared against a per-agent rolling baseline (Welford running mean + variance).";
50689
+ classifier;
50690
+ explicitClassifier;
50691
+ minSamplesForPrediction;
50692
+ constructor(opts) {
50693
+ super();
50694
+ this.explicitClassifier = opts?.classifier !== void 0;
50695
+ if (opts?.classifier) {
50696
+ this.classifier = opts.classifier;
50697
+ } else {
50698
+ this.classifier = new PendingClassifier();
50699
+ }
50700
+ this.minSamplesForPrediction = opts?.minSamplesForPrediction;
50701
+ }
50702
+ async subscribe(context) {
50703
+ await super.subscribe(context);
50704
+ if (this.explicitClassifier) return;
50705
+ const stateStore = new ClassifierStateStore({
50706
+ storage: context.storage,
50707
+ masterKey: context.masterKey,
50708
+ fortressId: context.fortressId,
50709
+ now: context.now
50710
+ });
50711
+ const realClassifier = new RollingBaselineClassifier({
50712
+ stateStore,
50713
+ ...this.minSamplesForPrediction !== void 0 ? { minSamplesForPrediction: this.minSamplesForPrediction } : {}
50714
+ });
50715
+ this.classifier = realClassifier;
50716
+ }
50717
+ async featureExtract(context) {
50718
+ return extractPerAgentActivity(context);
50719
+ }
50720
+ };
50721
+ PendingClassifier = class {
50722
+ classifierId = "pending";
50723
+ async observe() {
50724
+ throw new Error(
50725
+ "anomaly-detector: classifier accessed before subscribe()"
50726
+ );
50727
+ }
50728
+ async predict() {
50729
+ throw new Error(
50730
+ "anomaly-detector: classifier accessed before subscribe()"
50731
+ );
50732
+ }
50733
+ async train() {
50734
+ throw new Error(
50735
+ "anomaly-detector: classifier accessed before subscribe()"
50736
+ );
50737
+ }
50738
+ };
50739
+ }
50740
+ });
50741
+
50742
+ // src/anomaly-detection/anomaly-catalog.ts
50743
+ function findCatalogEntry(detectorId, classifierId) {
50744
+ return ANOMALY_CATALOG.find(
50745
+ (e) => e.detectorId === detectorId && e.classifierId === classifierId
50746
+ );
50747
+ }
50748
+ var ANOMALY_CATALOG;
50749
+ var init_anomaly_catalog = __esm({
50750
+ "src/anomaly-detection/anomaly-catalog.ts"() {
50751
+ init_per_agent_activity_detector();
50752
+ init_rolling_baseline();
50753
+ ANOMALY_CATALOG = [
50754
+ {
50755
+ detectorId: PER_AGENT_ACTIVITY_DETECTOR_ID,
50756
+ classifierId: ROLLING_BASELINE_CLASSIFIER_ID,
50757
+ description: "Per-agent statistical drift detector: tool-call count, egress volume, credential-use rate, audit-event count, recent-receipt count over a 24h rolling window. Welford running mean + variance baseline per agent.",
50758
+ factory: () => new PerAgentActivityDetector()
50759
+ }
50760
+ ];
50761
+ }
50762
+ });
50763
+ function anomalySubscriptionsPath(storagePath) {
50764
+ return join(storagePath, "anomaly-subscriptions.json");
50765
+ }
50766
+ async function loadAnomalySubscriptions(storagePath) {
50767
+ const filePath = anomalySubscriptionsPath(storagePath);
50768
+ try {
50769
+ const raw = await readFile(filePath, "utf8");
50770
+ const parsed = JSON.parse(raw);
50771
+ if (parsed.version !== FILE_VERSION2) return [];
50772
+ if (!Array.isArray(parsed.subscribed)) return [];
50773
+ return parsed.subscribed.filter(
50774
+ (t) => t !== null && typeof t === "object" && typeof t.detector_id === "string" && typeof t.classifier_id === "string"
50775
+ );
50776
+ } catch {
50777
+ return [];
50778
+ }
50779
+ }
50780
+ async function saveAnomalySubscriptions(storagePath, subscriptions) {
50781
+ const filePath = anomalySubscriptionsPath(storagePath);
50782
+ await mkdir(dirname(filePath), { recursive: true });
50783
+ const payload = {
50784
+ version: FILE_VERSION2,
50785
+ // Deduplicate.
50786
+ subscribed: dedupe(subscriptions)
50787
+ };
50788
+ await writeFile(filePath, JSON.stringify(payload, null, 2), {
50789
+ mode: 384
50790
+ });
50791
+ }
50792
+ function dedupe(items) {
50793
+ const seen = /* @__PURE__ */ new Set();
50794
+ const out = [];
50795
+ for (const item of items) {
50796
+ const key = `${item.detector_id}|${item.classifier_id}`;
50797
+ if (seen.has(key)) continue;
50798
+ seen.add(key);
50799
+ out.push(item);
50800
+ }
50801
+ return out;
50802
+ }
50803
+ var FILE_VERSION2;
50804
+ var init_anomaly_subscription_store = __esm({
50805
+ "src/anomaly-detection/anomaly-subscription-store.ts"() {
50806
+ FILE_VERSION2 = 1;
50807
+ }
50808
+ });
50809
+
50810
+ // src/cli/anomaly.ts
50811
+ var anomaly_exports = {};
50812
+ __export(anomaly_exports, {
50813
+ runAnomalyCommand: () => runAnomalyCommand
50814
+ });
50815
+ async function runAnomalyCommand(args) {
50816
+ const out = args.out ?? process.stdout;
50817
+ const err = args.err ?? process.stderr;
50818
+ const [sub, ...rest] = args.argv;
50819
+ if (!sub || sub === "--help" || sub === "-h") {
50820
+ printUsage8(out);
50821
+ return 0;
50822
+ }
50823
+ try {
50824
+ switch (sub) {
50825
+ case "detectors":
50826
+ return cmdDetectors(rest, { out });
50827
+ case "list-subscribed":
50828
+ return await cmdListSubscribed2({ out, args });
50829
+ case "subscribe":
50830
+ return await cmdSubscribe2(rest, { out, err, args });
50831
+ case "unsubscribe":
50832
+ return await cmdUnsubscribe2(rest, { out, err, args });
50833
+ case "findings":
50834
+ return await cmdFindings2(rest, { out, err, args });
50835
+ case "classifier-state":
50836
+ return await cmdClassifierState(rest, { out, err, args });
50837
+ default:
50838
+ err.write(`Unknown subcommand: ${sub}
50839
+ `);
50840
+ printUsage8(err);
50841
+ return 2;
50842
+ }
50843
+ } catch (e) {
50844
+ const msg = e instanceof Error ? e.message : String(e);
50845
+ err.write(`sanctuary anomaly: ${msg}
50846
+ `);
50847
+ return 1;
50848
+ }
50849
+ }
50850
+ function printUsage8(s) {
50851
+ s.write(`Usage: sanctuary anomaly <command> [args]
50852
+
50853
+ detectors list Catalog of available
50854
+ detector + classifier tuples.
50855
+ list-subscribed Subscriptions on this fortress.
50856
+ subscribe <detector-id> --classifier <id>
50857
+ Opt in. Writes the
50858
+ subscription file; the server
50859
+ picks it up on next boot.
50860
+ unsubscribe <detector-id> --classifier <id>
50861
+ Opt out.
50862
+ findings [opts] Read anomaly findings.
50863
+ --since <iso> observed_at >= iso.
50864
+ --severity <info|warn|alert> Filter by severity.
50865
+ --detector-id <id> Filter by emitting detector.
50866
+ --agent-id <id> Filter by agent attribution.
50867
+ --limit <n> Cap result count (default 100).
50868
+ findings show <finding-id> Full drift-inspector detail.
50869
+ classifier-state <detector-id> --classifier <id>
50870
+ Per-agent training state.
50871
+ `);
50872
+ }
50873
+ function flagValue4(argv, name) {
50874
+ const i = argv.indexOf(name);
50875
+ if (i === -1) return void 0;
50876
+ return argv[i + 1];
50877
+ }
50878
+ function cmdDetectors(argv, ctx) {
50879
+ const sub = argv[0];
50880
+ if (sub !== void 0 && sub !== "list") {
50881
+ ctx.out.write(`Unknown detectors subcommand: ${sub}
50882
+ `);
50883
+ return 2;
50884
+ }
50885
+ if (ANOMALY_CATALOG.length === 0) {
50886
+ ctx.out.write("(no detectors registered)\n");
50887
+ return 0;
50888
+ }
50889
+ for (const entry of ANOMALY_CATALOG) {
50890
+ ctx.out.write(
50891
+ `${entry.detectorId} [classifier: ${entry.classifierId}]
50892
+ ${entry.description}
50893
+ `
50894
+ );
50895
+ }
50896
+ return 0;
50897
+ }
50898
+ async function cmdListSubscribed2(ctx) {
50899
+ const storagePath = await resolveStoragePath3(ctx.args);
50900
+ const subscribed = await loadAnomalySubscriptions(storagePath);
50901
+ if (subscribed.length === 0) {
50902
+ ctx.out.write("(no subscriptions)\n");
50903
+ return 0;
50904
+ }
50905
+ for (const t of subscribed) {
50906
+ ctx.out.write(`${t.detector_id} [classifier: ${t.classifier_id}]
50907
+ `);
50908
+ }
50909
+ return 0;
50910
+ }
50911
+ async function cmdSubscribe2(argv, ctx) {
50912
+ const detectorId = argv[0];
50913
+ const classifierId = flagValue4(argv, "--classifier");
50914
+ if (!detectorId) {
50915
+ ctx.err.write("subscribe requires a detector-id\n");
50916
+ return 2;
50917
+ }
50918
+ if (!classifierId) {
50919
+ ctx.err.write("subscribe requires --classifier <id>\n");
50920
+ return 2;
50921
+ }
50922
+ const entry = findCatalogEntry(detectorId, classifierId);
50923
+ if (!entry) {
50924
+ ctx.err.write(
50925
+ `Unknown detector/classifier pair: ${detectorId} / ${classifierId}
50926
+ `
50927
+ );
50928
+ return 2;
50929
+ }
50930
+ const storagePath = await resolveStoragePath3(ctx.args);
50931
+ const subscribed = await loadAnomalySubscriptions(storagePath);
50932
+ const exists = subscribed.some(
50933
+ (t) => t.detector_id === detectorId && t.classifier_id === classifierId
50934
+ );
50935
+ if (exists) {
50936
+ ctx.out.write(
50937
+ `Already subscribed: ${detectorId} [classifier: ${classifierId}]
50938
+ `
50939
+ );
50940
+ return 0;
50941
+ }
50942
+ subscribed.push({ detector_id: detectorId, classifier_id: classifierId });
50943
+ await saveAnomalySubscriptions(storagePath, subscribed);
50944
+ ctx.out.write(
50945
+ `Subscribed: ${detectorId} [classifier: ${classifierId}]
50946
+ Restart Sanctuary or wait for the next dispatcher tick.
50947
+ `
50948
+ );
50949
+ return 0;
50950
+ }
50951
+ async function cmdUnsubscribe2(argv, ctx) {
50952
+ const detectorId = argv[0];
50953
+ const classifierId = flagValue4(argv, "--classifier");
50954
+ if (!detectorId) {
50955
+ ctx.err.write("unsubscribe requires a detector-id\n");
50956
+ return 2;
50957
+ }
50958
+ if (!classifierId) {
50959
+ ctx.err.write("unsubscribe requires --classifier <id>\n");
50960
+ return 2;
50961
+ }
50962
+ const storagePath = await resolveStoragePath3(ctx.args);
50963
+ const subscribed = await loadAnomalySubscriptions(storagePath);
50964
+ const filtered = subscribed.filter(
50965
+ (t) => !(t.detector_id === detectorId && t.classifier_id === classifierId)
50966
+ );
50967
+ if (filtered.length === subscribed.length) {
50968
+ ctx.out.write(
50969
+ `Not subscribed: ${detectorId} [classifier: ${classifierId}]
50970
+ `
50971
+ );
50972
+ return 0;
50973
+ }
50974
+ await saveAnomalySubscriptions(storagePath, filtered);
50975
+ ctx.out.write(
50976
+ `Unsubscribed: ${detectorId} [classifier: ${classifierId}]
50977
+ `
50978
+ );
50979
+ return 0;
50980
+ }
50981
+ async function cmdFindings2(argv, ctx) {
50982
+ if (argv[0] === "show") {
50983
+ return await cmdFindingsShow(argv.slice(1), ctx);
50984
+ }
50985
+ const filters = parseFindingFilters2(argv);
50986
+ const masterKey = await deriveFortressMasterKey(ctx);
50987
+ const storagePath = await resolveStoragePath3(ctx.args);
50988
+ const storage = new FilesystemStorage(`${storagePath}/state`);
50989
+ const fortressId = fortressIdFromStoragePath(storagePath);
50990
+ const store = new SentinelFindingStore({
50991
+ storage,
50992
+ masterKey,
50993
+ fortressId
50994
+ });
50995
+ const filterSentinelId = filters.detectorId !== void 0 ? `${ANOMALY_SENTINEL_ID_PREFIX}${filters.detectorId}` : void 0;
50996
+ const allFindings = await store.listFindings({
50997
+ limit: filters.limit ?? 100,
50998
+ ...filters.since !== void 0 ? { since: filters.since } : {},
50999
+ ...filters.severity !== void 0 ? { severity: filters.severity } : {},
51000
+ ...filterSentinelId !== void 0 ? { sentinelId: filterSentinelId } : {},
51001
+ ...filters.agentId !== void 0 ? { agentId: filters.agentId } : {}
51002
+ });
51003
+ const anomalyFindings = filterSentinelId !== void 0 ? allFindings : allFindings.filter(
51004
+ (f) => f.sentinel_id.startsWith(ANOMALY_SENTINEL_ID_PREFIX)
51005
+ );
51006
+ if (anomalyFindings.length === 0) {
51007
+ ctx.out.write("(no findings)\n");
51008
+ return 0;
51009
+ }
51010
+ for (const finding of anomalyFindings) {
51011
+ const detectorId = finding.details["detector_id"] ?? "";
51012
+ const score = finding.details["anomaly_score"];
51013
+ const scoreStr = typeof score === "number" ? ` score=${score.toFixed(2)}` : "";
51014
+ ctx.out.write(
51015
+ `[${finding.observed_at}] ${finding.severity.toUpperCase()} ${detectorId}${finding.agent_id ? ` (agent ${finding.agent_id})` : ""}${scoreStr}: ${finding.summary}
51016
+ `
51017
+ );
51018
+ }
51019
+ return 0;
51020
+ }
51021
+ async function cmdFindingsShow(argv, ctx) {
51022
+ const findingId = argv[0];
51023
+ if (!findingId) {
51024
+ ctx.err.write("findings show requires a finding-id\n");
51025
+ return 2;
51026
+ }
51027
+ const masterKey = await deriveFortressMasterKey(ctx);
51028
+ const storagePath = await resolveStoragePath3(ctx.args);
51029
+ const storage = new FilesystemStorage(`${storagePath}/state`);
51030
+ const fortressId = fortressIdFromStoragePath(storagePath);
51031
+ const store = new SentinelFindingStore({ storage, masterKey, fortressId });
51032
+ const finding = await store.loadFinding(findingId);
51033
+ if (!finding) {
51034
+ ctx.err.write(`Finding not found: ${findingId}
51035
+ `);
51036
+ return 1;
51037
+ }
51038
+ if (!finding.sentinel_id.startsWith(ANOMALY_SENTINEL_ID_PREFIX)) {
51039
+ ctx.err.write(
51040
+ `Finding ${findingId} is not an anomaly finding; try sanctuary sentinel findings.
51041
+ `
51042
+ );
51043
+ return 1;
51044
+ }
51045
+ ctx.out.write(JSON.stringify(finding, null, 2) + "\n");
51046
+ return 0;
51047
+ }
51048
+ async function cmdClassifierState(argv, ctx) {
51049
+ const detectorId = argv[0];
51050
+ const classifierId = flagValue4(argv, "--classifier");
51051
+ if (!detectorId) {
51052
+ ctx.err.write("classifier-state requires a detector-id\n");
51053
+ return 2;
51054
+ }
51055
+ if (!classifierId) {
51056
+ ctx.err.write("classifier-state requires --classifier <id>\n");
51057
+ return 2;
51058
+ }
51059
+ const entry = findCatalogEntry(detectorId, classifierId);
51060
+ if (!entry) {
51061
+ ctx.err.write(
51062
+ `Unknown detector/classifier pair: ${detectorId} / ${classifierId}
51063
+ `
51064
+ );
51065
+ return 2;
51066
+ }
51067
+ const masterKey = await deriveFortressMasterKey(ctx);
51068
+ const storagePath = await resolveStoragePath3(ctx.args);
51069
+ const storage = new FilesystemStorage(`${storagePath}/state`);
51070
+ const fortressId = fortressIdFromStoragePath(storagePath);
51071
+ const stateStore = new ClassifierStateStore({
51072
+ storage,
51073
+ masterKey,
51074
+ fortressId
51075
+ });
51076
+ const agentIds = await stateStore.listAgents(classifierId);
51077
+ if (agentIds.length === 0) {
51078
+ ctx.out.write("(no classifier state yet)\n");
51079
+ return 0;
51080
+ }
51081
+ for (const agentId of agentIds) {
51082
+ try {
51083
+ const raw = await stateStore.loadState(classifierId, agentId);
51084
+ if (raw === null) continue;
51085
+ const sampleCount = typeof raw.sample_count === "number" ? raw.sample_count : "?";
51086
+ ctx.out.write(`${agentId}: sample_count=${sampleCount}
51087
+ `);
51088
+ } catch {
51089
+ ctx.out.write(`${agentId}: (load failed)
51090
+ `);
51091
+ }
51092
+ }
51093
+ return 0;
51094
+ }
51095
+ function parseFindingFilters2(argv) {
51096
+ const filters = {};
51097
+ for (let i = 0; i < argv.length; i += 1) {
51098
+ const arg = argv[i];
51099
+ if (arg === "--since" && argv[i + 1]) {
51100
+ filters.since = argv[++i];
51101
+ } else if (arg === "--severity" && argv[i + 1]) {
51102
+ const next = argv[++i];
51103
+ if (next === "info" || next === "warn" || next === "alert") {
51104
+ filters.severity = next;
51105
+ }
51106
+ } else if (arg === "--detector-id" && argv[i + 1]) {
51107
+ filters.detectorId = argv[++i];
51108
+ } else if (arg === "--agent-id" && argv[i + 1]) {
51109
+ filters.agentId = argv[++i];
51110
+ } else if (arg === "--limit" && argv[i + 1]) {
51111
+ const n = Number.parseInt(argv[++i], 10);
51112
+ if (!Number.isNaN(n) && n > 0) filters.limit = n;
51113
+ }
51114
+ }
51115
+ return filters;
51116
+ }
51117
+ async function resolveStoragePath3(args) {
51118
+ if (args.storagePath) return args.storagePath;
51119
+ const config = await loadConfig();
51120
+ return config.storage_path;
51121
+ }
51122
+ async function deriveFortressMasterKey(ctx) {
51123
+ const storagePath = await resolveStoragePath3(ctx.args);
51124
+ const storage = new FilesystemStorage(`${storagePath}/state`);
51125
+ let passphrase = ctx.args.passphrase ?? process.env["SANCTUARY_PASSPHRASE"];
51126
+ if (!passphrase) {
51127
+ const resolved = await getOrCreatePassphrase();
51128
+ passphrase = resolved.value;
51129
+ }
51130
+ let existingParams;
51131
+ try {
51132
+ const raw = await storage.read("_meta", "key-params");
51133
+ if (raw) existingParams = JSON.parse(bytesToString(raw));
51134
+ } catch {
51135
+ }
51136
+ const { key: masterKey, params } = await deriveMasterKey(
51137
+ passphrase,
51138
+ existingParams
51139
+ );
51140
+ if (!existingParams) {
51141
+ await storage.write(
51142
+ "_meta",
51143
+ "key-params",
51144
+ stringToBytes(JSON.stringify(params))
51145
+ );
51146
+ }
51147
+ return masterKey;
51148
+ }
51149
+ var init_anomaly = __esm({
51150
+ "src/cli/anomaly.ts"() {
51151
+ init_config();
51152
+ init_filesystem();
51153
+ init_key_derivation();
51154
+ init_encoding();
51155
+ init_passphrase();
51156
+ init_wiring();
51157
+ init_sentinel_finding_store();
51158
+ init_anomaly_catalog();
51159
+ init_anomaly_subscription_store();
51160
+ init_classifier_state_store();
51161
+ init_types4();
51162
+ }
51163
+ });
51164
+
48996
51165
  // src/mcp/broker-server.ts
48997
51166
  var broker_server_exports = {};
48998
51167
  __export(broker_server_exports, {
@@ -49862,6 +52031,11 @@ async function main() {
49862
52031
  const code = await runDidWebCommand2({ argv: args.slice(1) });
49863
52032
  process.exit(code);
49864
52033
  }
52034
+ if (args[0] === "anomaly") {
52035
+ const { runAnomalyCommand: runAnomalyCommand2 } = await Promise.resolve().then(() => (init_anomaly(), anomaly_exports));
52036
+ const code = await runAnomalyCommand2({ argv: args.slice(1) });
52037
+ process.exit(code);
52038
+ }
49865
52039
  if (args[0] === "broker-server") {
49866
52040
  const { openBroker: openBroker2 } = await Promise.resolve().then(() => (init_open(), open_exports));
49867
52041
  const { createBrokerMcpServer: createBrokerMcpServer2 } = await Promise.resolve().then(() => (init_broker_server(), broker_server_exports));