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