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