@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/cli.cjs
CHANGED
|
@@ -4457,9 +4457,35 @@ function validatePolicy(raw) {
|
|
|
4457
4457
|
};
|
|
4458
4458
|
delete merged.auto_deny;
|
|
4459
4459
|
return merged;
|
|
4460
|
-
})()
|
|
4460
|
+
})(),
|
|
4461
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4461
4462
|
};
|
|
4462
4463
|
}
|
|
4464
|
+
function parseApprovalRedirect(raw) {
|
|
4465
|
+
if (raw === void 0 || raw === null) {
|
|
4466
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4467
|
+
}
|
|
4468
|
+
if (typeof raw !== "object") {
|
|
4469
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4470
|
+
}
|
|
4471
|
+
const obj = raw;
|
|
4472
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4473
|
+
const modeRaw = obj.mode;
|
|
4474
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4475
|
+
if (modeRaw !== void 0) {
|
|
4476
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4477
|
+
throw new Error(
|
|
4478
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4479
|
+
);
|
|
4480
|
+
}
|
|
4481
|
+
mode = modeRaw;
|
|
4482
|
+
}
|
|
4483
|
+
const result = { enabled, mode };
|
|
4484
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4485
|
+
result.per_agent = obj.per_agent;
|
|
4486
|
+
}
|
|
4487
|
+
return result;
|
|
4488
|
+
}
|
|
4463
4489
|
function generateDefaultPolicyYaml() {
|
|
4464
4490
|
return `# Sanctuary Principal Policy v1
|
|
4465
4491
|
# This file controls what your agent can do without asking.
|
|
@@ -4538,6 +4564,7 @@ tier3_always_allow:
|
|
|
4538
4564
|
- handshake_status
|
|
4539
4565
|
- handshake_exchange
|
|
4540
4566
|
- handshake_verify_attestation
|
|
4567
|
+
- handshake_abort
|
|
4541
4568
|
- reputation_query_weighted
|
|
4542
4569
|
- federation_peers
|
|
4543
4570
|
- federation_trust_evaluate
|
|
@@ -4572,6 +4599,21 @@ tier3_always_allow:
|
|
|
4572
4599
|
approval_channel:
|
|
4573
4600
|
type: stderr
|
|
4574
4601
|
timeout_seconds: 300
|
|
4602
|
+
|
|
4603
|
+
# \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
|
|
4604
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4605
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4606
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4607
|
+
#
|
|
4608
|
+
# mode:
|
|
4609
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4610
|
+
# decision from the inbox (default once enabled).
|
|
4611
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4612
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4613
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4614
|
+
approval_redirect:
|
|
4615
|
+
enabled: false
|
|
4616
|
+
mode: replace
|
|
4575
4617
|
`;
|
|
4576
4618
|
}
|
|
4577
4619
|
async function loadPrincipalPolicy(storagePath) {
|
|
@@ -4608,7 +4650,7 @@ async function loadPrincipalPolicy(storagePath) {
|
|
|
4608
4650
|
);
|
|
4609
4651
|
}
|
|
4610
4652
|
}
|
|
4611
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4653
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_APPROVAL_REDIRECT, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4612
4654
|
var init_loader = __esm({
|
|
4613
4655
|
"src/principal-policy/loader.ts"() {
|
|
4614
4656
|
DEFAULT_TIER2 = {
|
|
@@ -4625,6 +4667,10 @@ var init_loader = __esm({
|
|
|
4625
4667
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4626
4668
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4627
4669
|
};
|
|
4670
|
+
DEFAULT_APPROVAL_REDIRECT = {
|
|
4671
|
+
enabled: false,
|
|
4672
|
+
mode: "replace"
|
|
4673
|
+
};
|
|
4628
4674
|
DEFAULT_POLICY = {
|
|
4629
4675
|
version: 1,
|
|
4630
4676
|
tier1_always_approve: [
|
|
@@ -4698,6 +4744,7 @@ var init_loader = __esm({
|
|
|
4698
4744
|
"handshake_status",
|
|
4699
4745
|
"handshake_exchange",
|
|
4700
4746
|
"handshake_verify_attestation",
|
|
4747
|
+
"handshake_abort",
|
|
4701
4748
|
"reputation_query_weighted",
|
|
4702
4749
|
"federation_peers",
|
|
4703
4750
|
"federation_trust_evaluate",
|
|
@@ -4739,7 +4786,8 @@ var init_loader = __esm({
|
|
|
4739
4786
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4740
4787
|
// Read-only; rule-based Annex III classifier
|
|
4741
4788
|
],
|
|
4742
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4789
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4790
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4743
4791
|
};
|
|
4744
4792
|
MalformedPrincipalPolicyError = class extends Error {
|
|
4745
4793
|
constructor(policyPath, reason) {
|
|
@@ -20570,6 +20618,174 @@ var init_approval_aggregator = __esm({
|
|
|
20570
20618
|
}
|
|
20571
20619
|
});
|
|
20572
20620
|
|
|
20621
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
20622
|
+
function auditEntryIdFor(request) {
|
|
20623
|
+
return `${request.timestamp}:${request.operation}`;
|
|
20624
|
+
}
|
|
20625
|
+
function statusToDecision(entry) {
|
|
20626
|
+
switch (entry.status) {
|
|
20627
|
+
case "approved":
|
|
20628
|
+
return {
|
|
20629
|
+
decision: "approve",
|
|
20630
|
+
decided_by: "human"
|
|
20631
|
+
};
|
|
20632
|
+
case "denied":
|
|
20633
|
+
return {
|
|
20634
|
+
decision: "deny",
|
|
20635
|
+
decided_by: "human"
|
|
20636
|
+
};
|
|
20637
|
+
case "timeout":
|
|
20638
|
+
case "expired":
|
|
20639
|
+
return {
|
|
20640
|
+
decision: "deny",
|
|
20641
|
+
decided_by: "timeout"
|
|
20642
|
+
};
|
|
20643
|
+
default:
|
|
20644
|
+
return null;
|
|
20645
|
+
}
|
|
20646
|
+
}
|
|
20647
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
20648
|
+
return (_request) => {
|
|
20649
|
+
const cfg = supplier().approval_redirect;
|
|
20650
|
+
if (!cfg || cfg.enabled !== true) {
|
|
20651
|
+
return { enabled: false, mode: "replace" };
|
|
20652
|
+
}
|
|
20653
|
+
return {
|
|
20654
|
+
enabled: true,
|
|
20655
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
20656
|
+
};
|
|
20657
|
+
};
|
|
20658
|
+
}
|
|
20659
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS, AggregatorBackedChannel;
|
|
20660
|
+
var init_aggregator_backed_channel = __esm({
|
|
20661
|
+
"src/principal-policy/channels/aggregator-backed-channel.ts"() {
|
|
20662
|
+
DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
20663
|
+
AggregatorBackedChannel = class {
|
|
20664
|
+
underlying;
|
|
20665
|
+
aggregator;
|
|
20666
|
+
resolveRedirect;
|
|
20667
|
+
replaceModeTimeoutMs;
|
|
20668
|
+
now;
|
|
20669
|
+
constructor(opts) {
|
|
20670
|
+
this.underlying = opts.underlying;
|
|
20671
|
+
this.aggregator = opts.aggregator;
|
|
20672
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
20673
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
20674
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
20675
|
+
}
|
|
20676
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
20677
|
+
getUnderlying() {
|
|
20678
|
+
return this.underlying;
|
|
20679
|
+
}
|
|
20680
|
+
async requestApproval(request) {
|
|
20681
|
+
const cfg = this.resolveRedirect(request);
|
|
20682
|
+
if (!cfg.enabled) {
|
|
20683
|
+
return this.underlying.requestApproval(request);
|
|
20684
|
+
}
|
|
20685
|
+
if (cfg.mode === "replace") {
|
|
20686
|
+
return this.awaitAggregatorDecision(request);
|
|
20687
|
+
}
|
|
20688
|
+
return this.notifyMode(request);
|
|
20689
|
+
}
|
|
20690
|
+
/**
|
|
20691
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
20692
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
20693
|
+
* between list and subscribe). Match incoming events to this request by
|
|
20694
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
20695
|
+
*/
|
|
20696
|
+
async awaitAggregatorDecision(request) {
|
|
20697
|
+
const auditId = auditEntryIdFor(request);
|
|
20698
|
+
return new Promise((resolveOuter) => {
|
|
20699
|
+
let settled = false;
|
|
20700
|
+
let unsubscribe = null;
|
|
20701
|
+
let timeoutHandle = null;
|
|
20702
|
+
const settle = (response) => {
|
|
20703
|
+
if (settled) return;
|
|
20704
|
+
settled = true;
|
|
20705
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
20706
|
+
if (unsubscribe) {
|
|
20707
|
+
try {
|
|
20708
|
+
unsubscribe();
|
|
20709
|
+
} catch {
|
|
20710
|
+
}
|
|
20711
|
+
}
|
|
20712
|
+
resolveOuter(response);
|
|
20713
|
+
};
|
|
20714
|
+
const onEvent = (emit) => {
|
|
20715
|
+
if (emit.type !== "resolved") return;
|
|
20716
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
20717
|
+
const mapped = statusToDecision(emit.entry);
|
|
20718
|
+
if (!mapped) return;
|
|
20719
|
+
settle({
|
|
20720
|
+
decision: mapped.decision,
|
|
20721
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
20722
|
+
decided_by: mapped.decided_by
|
|
20723
|
+
});
|
|
20724
|
+
};
|
|
20725
|
+
try {
|
|
20726
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
20727
|
+
} catch (err) {
|
|
20728
|
+
settle({
|
|
20729
|
+
decision: "deny",
|
|
20730
|
+
decided_at: this.now().toISOString(),
|
|
20731
|
+
decided_by: "channel_failure"
|
|
20732
|
+
});
|
|
20733
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
20734
|
+
}
|
|
20735
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
20736
|
+
for (const entry of entries) {
|
|
20737
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
20738
|
+
const mapped = statusToDecision(entry);
|
|
20739
|
+
if (!mapped) return;
|
|
20740
|
+
settle({
|
|
20741
|
+
decision: mapped.decision,
|
|
20742
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
20743
|
+
decided_by: mapped.decided_by
|
|
20744
|
+
});
|
|
20745
|
+
return;
|
|
20746
|
+
}
|
|
20747
|
+
}).catch(() => {
|
|
20748
|
+
});
|
|
20749
|
+
timeoutHandle = setTimeout(() => {
|
|
20750
|
+
settle({
|
|
20751
|
+
decision: "deny",
|
|
20752
|
+
decided_at: this.now().toISOString(),
|
|
20753
|
+
decided_by: "timeout"
|
|
20754
|
+
});
|
|
20755
|
+
}, this.replaceModeTimeoutMs);
|
|
20756
|
+
});
|
|
20757
|
+
}
|
|
20758
|
+
/**
|
|
20759
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
20760
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
20761
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
20762
|
+
* downstream audit logging is unchanged.
|
|
20763
|
+
*
|
|
20764
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
20765
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
20766
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
20767
|
+
*/
|
|
20768
|
+
async notifyMode(request) {
|
|
20769
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
20770
|
+
let underlyingPromise;
|
|
20771
|
+
try {
|
|
20772
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
20773
|
+
} catch (err) {
|
|
20774
|
+
const response = await aggregatorPromise;
|
|
20775
|
+
return response;
|
|
20776
|
+
}
|
|
20777
|
+
return Promise.race([
|
|
20778
|
+
aggregatorPromise,
|
|
20779
|
+
underlyingPromise.catch(
|
|
20780
|
+
() => new Promise(() => {
|
|
20781
|
+
})
|
|
20782
|
+
)
|
|
20783
|
+
]);
|
|
20784
|
+
}
|
|
20785
|
+
};
|
|
20786
|
+
}
|
|
20787
|
+
});
|
|
20788
|
+
|
|
20573
20789
|
// src/principal-policy/tools.ts
|
|
20574
20790
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
20575
20791
|
return [
|
|
@@ -21473,6 +21689,76 @@ var init_attestation = __esm({
|
|
|
21473
21689
|
}
|
|
21474
21690
|
});
|
|
21475
21691
|
|
|
21692
|
+
// src/handshake/audit.ts
|
|
21693
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
21694
|
+
auditLog.append(
|
|
21695
|
+
"l4",
|
|
21696
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
21697
|
+
ctx.identity_id,
|
|
21698
|
+
detailsFromContext(ctx),
|
|
21699
|
+
"success"
|
|
21700
|
+
);
|
|
21701
|
+
}
|
|
21702
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
21703
|
+
const details = detailsFromContext(ctx);
|
|
21704
|
+
if (ctx.trust_tier !== void 0) {
|
|
21705
|
+
details.trust_tier = ctx.trust_tier;
|
|
21706
|
+
}
|
|
21707
|
+
auditLog.append(
|
|
21708
|
+
"l4",
|
|
21709
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
21710
|
+
ctx.identity_id,
|
|
21711
|
+
details,
|
|
21712
|
+
"success"
|
|
21713
|
+
);
|
|
21714
|
+
}
|
|
21715
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
21716
|
+
const details = detailsFromContext(ctx);
|
|
21717
|
+
details.reason = ctx.reason;
|
|
21718
|
+
if (ctx.error !== void 0) {
|
|
21719
|
+
details.error = ctx.error;
|
|
21720
|
+
}
|
|
21721
|
+
auditLog.append(
|
|
21722
|
+
"l4",
|
|
21723
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
21724
|
+
ctx.identity_id,
|
|
21725
|
+
details,
|
|
21726
|
+
"failure"
|
|
21727
|
+
);
|
|
21728
|
+
}
|
|
21729
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
21730
|
+
const details = detailsFromContext(ctx);
|
|
21731
|
+
details.reason = ctx.reason;
|
|
21732
|
+
auditLog.append(
|
|
21733
|
+
"l4",
|
|
21734
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
21735
|
+
ctx.identity_id,
|
|
21736
|
+
details,
|
|
21737
|
+
"failure"
|
|
21738
|
+
);
|
|
21739
|
+
}
|
|
21740
|
+
function detailsFromContext(ctx) {
|
|
21741
|
+
const details = {
|
|
21742
|
+
session_id: ctx.session_id,
|
|
21743
|
+
role: ctx.role
|
|
21744
|
+
};
|
|
21745
|
+
if (ctx.counterparty_id !== void 0) {
|
|
21746
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
21747
|
+
}
|
|
21748
|
+
return details;
|
|
21749
|
+
}
|
|
21750
|
+
var HANDSHAKE_LIFECYCLE_OPS;
|
|
21751
|
+
var init_audit = __esm({
|
|
21752
|
+
"src/handshake/audit.ts"() {
|
|
21753
|
+
HANDSHAKE_LIFECYCLE_OPS = {
|
|
21754
|
+
INITIATED: "handshake_initiated",
|
|
21755
|
+
COMPLETED: "handshake_completed",
|
|
21756
|
+
FAILED: "handshake_failed",
|
|
21757
|
+
ABORTED: "handshake_aborted"
|
|
21758
|
+
};
|
|
21759
|
+
}
|
|
21760
|
+
});
|
|
21761
|
+
|
|
21476
21762
|
// src/handshake/tools.ts
|
|
21477
21763
|
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
21478
21764
|
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
@@ -21506,6 +21792,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21506
21792
|
const { challenge, session } = initiateHandshake(shr);
|
|
21507
21793
|
sessions.set(session.session_id, session);
|
|
21508
21794
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
21795
|
+
auditHandshakeInitiated(auditLog, {
|
|
21796
|
+
session_id: session.session_id,
|
|
21797
|
+
role: "initiator",
|
|
21798
|
+
identity_id: shr.body.instance_id
|
|
21799
|
+
});
|
|
21509
21800
|
return toolResult({
|
|
21510
21801
|
session_id: session.session_id,
|
|
21511
21802
|
challenge,
|
|
@@ -21545,10 +21836,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21545
21836
|
);
|
|
21546
21837
|
if ("error" in result) {
|
|
21547
21838
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
21839
|
+
auditHandshakeFailed(auditLog, {
|
|
21840
|
+
session_id: "unknown",
|
|
21841
|
+
role: "responder",
|
|
21842
|
+
identity_id: shr.body.instance_id,
|
|
21843
|
+
reason: classifyRespondFailure(result.error),
|
|
21844
|
+
error: result.error
|
|
21845
|
+
});
|
|
21548
21846
|
return toolResult({ error: result.error });
|
|
21549
21847
|
}
|
|
21550
21848
|
sessions.set(result.session.session_id, result.session);
|
|
21551
21849
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
21850
|
+
auditHandshakeInitiated(auditLog, {
|
|
21851
|
+
session_id: result.session.session_id,
|
|
21852
|
+
role: "responder",
|
|
21853
|
+
identity_id: shr.body.instance_id,
|
|
21854
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
21855
|
+
});
|
|
21552
21856
|
let autoPublishResult;
|
|
21553
21857
|
if (autoPublishHandshakes) {
|
|
21554
21858
|
autoPublishResult = { attempted: true };
|
|
@@ -21656,9 +21960,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21656
21960
|
const response = args.response;
|
|
21657
21961
|
const session = sessions.get(sessionId);
|
|
21658
21962
|
if (!session) {
|
|
21963
|
+
auditHandshakeFailed(auditLog, {
|
|
21964
|
+
session_id: sessionId,
|
|
21965
|
+
role: "initiator",
|
|
21966
|
+
identity_id: "unknown",
|
|
21967
|
+
reason: "session_unknown",
|
|
21968
|
+
error: `No handshake session found: ${sessionId}`
|
|
21969
|
+
});
|
|
21659
21970
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21660
21971
|
}
|
|
21661
21972
|
if (session.state !== "initiated") {
|
|
21973
|
+
auditHandshakeFailed(auditLog, {
|
|
21974
|
+
session_id: sessionId,
|
|
21975
|
+
role: "initiator",
|
|
21976
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21977
|
+
reason: "session_state_mismatch",
|
|
21978
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21979
|
+
});
|
|
21662
21980
|
return toolResult({
|
|
21663
21981
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21664
21982
|
});
|
|
@@ -21672,6 +21990,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21672
21990
|
if ("error" in result) {
|
|
21673
21991
|
session.state = "failed";
|
|
21674
21992
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
21993
|
+
auditHandshakeFailed(auditLog, {
|
|
21994
|
+
session_id: sessionId,
|
|
21995
|
+
role: "initiator",
|
|
21996
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21997
|
+
reason: classifyCompleteFailure(result.error),
|
|
21998
|
+
error: result.error
|
|
21999
|
+
});
|
|
21675
22000
|
return toolResult({ error: result.error });
|
|
21676
22001
|
}
|
|
21677
22002
|
session.state = "completed";
|
|
@@ -21680,6 +22005,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21680
22005
|
session.result = result.result;
|
|
21681
22006
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
21682
22007
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
22008
|
+
auditHandshakeCompleted(auditLog, {
|
|
22009
|
+
session_id: sessionId,
|
|
22010
|
+
role: "initiator",
|
|
22011
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22012
|
+
counterparty_id: result.result.counterparty_id,
|
|
22013
|
+
trust_tier: result.result.trust_tier
|
|
22014
|
+
});
|
|
21683
22015
|
return toolResult({
|
|
21684
22016
|
completion: result.completion,
|
|
21685
22017
|
result: result.result,
|
|
@@ -21727,6 +22059,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21727
22059
|
void 0,
|
|
21728
22060
|
result.verified ? "success" : "failure"
|
|
21729
22061
|
);
|
|
22062
|
+
if (result.verified) {
|
|
22063
|
+
auditHandshakeCompleted(auditLog, {
|
|
22064
|
+
session_id: session.session_id,
|
|
22065
|
+
role: "responder",
|
|
22066
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22067
|
+
counterparty_id: result.counterparty_id,
|
|
22068
|
+
trust_tier: result.trust_tier
|
|
22069
|
+
});
|
|
22070
|
+
} else {
|
|
22071
|
+
auditHandshakeFailed(auditLog, {
|
|
22072
|
+
session_id: session.session_id,
|
|
22073
|
+
role: "responder",
|
|
22074
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22075
|
+
counterparty_id: result.counterparty_id,
|
|
22076
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
22077
|
+
error: result.errors.join("; ")
|
|
22078
|
+
});
|
|
22079
|
+
}
|
|
21730
22080
|
return toolResult({ result });
|
|
21731
22081
|
}
|
|
21732
22082
|
return toolResult({
|
|
@@ -21834,10 +22184,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21834
22184
|
_content_trust: "external"
|
|
21835
22185
|
});
|
|
21836
22186
|
}
|
|
22187
|
+
},
|
|
22188
|
+
{
|
|
22189
|
+
name: "handshake_abort",
|
|
22190
|
+
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.",
|
|
22191
|
+
inputSchema: {
|
|
22192
|
+
type: "object",
|
|
22193
|
+
properties: {
|
|
22194
|
+
session_id: {
|
|
22195
|
+
type: "string",
|
|
22196
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
22197
|
+
},
|
|
22198
|
+
reason: {
|
|
22199
|
+
type: "string",
|
|
22200
|
+
enum: [
|
|
22201
|
+
"operator_cancelled",
|
|
22202
|
+
"session_timeout",
|
|
22203
|
+
"transport_dropped",
|
|
22204
|
+
"shutdown",
|
|
22205
|
+
"other"
|
|
22206
|
+
],
|
|
22207
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
22208
|
+
}
|
|
22209
|
+
},
|
|
22210
|
+
required: ["session_id"]
|
|
22211
|
+
},
|
|
22212
|
+
handler: async (args) => {
|
|
22213
|
+
const sessionId = args.session_id;
|
|
22214
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
22215
|
+
const session = sessions.get(sessionId);
|
|
22216
|
+
if (!session) {
|
|
22217
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
22218
|
+
}
|
|
22219
|
+
if (session.state === "completed") {
|
|
22220
|
+
return toolResult({
|
|
22221
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
22222
|
+
});
|
|
22223
|
+
}
|
|
22224
|
+
sessions.delete(sessionId);
|
|
22225
|
+
auditHandshakeAborted(auditLog, {
|
|
22226
|
+
session_id: sessionId,
|
|
22227
|
+
role: session.role,
|
|
22228
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22229
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
22230
|
+
reason
|
|
22231
|
+
});
|
|
22232
|
+
return toolResult({
|
|
22233
|
+
aborted: true,
|
|
22234
|
+
session_id: sessionId,
|
|
22235
|
+
reason
|
|
22236
|
+
});
|
|
22237
|
+
}
|
|
21837
22238
|
}
|
|
21838
22239
|
];
|
|
21839
22240
|
return { tools, handshakeResults };
|
|
21840
22241
|
}
|
|
22242
|
+
function classifyRespondFailure(error) {
|
|
22243
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22244
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
22245
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22246
|
+
return "other";
|
|
22247
|
+
}
|
|
22248
|
+
function classifyCompleteFailure(error) {
|
|
22249
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22250
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
22251
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
22252
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22253
|
+
return "other";
|
|
22254
|
+
}
|
|
21841
22255
|
var init_tools6 = __esm({
|
|
21842
22256
|
"src/handshake/tools.ts"() {
|
|
21843
22257
|
init_router();
|
|
@@ -21847,6 +22261,7 @@ var init_tools6 = __esm({
|
|
|
21847
22261
|
init_encoding();
|
|
21848
22262
|
init_protocol();
|
|
21849
22263
|
init_attestation();
|
|
22264
|
+
init_audit();
|
|
21850
22265
|
init_verifier();
|
|
21851
22266
|
}
|
|
21852
22267
|
});
|
|
@@ -32982,7 +33397,14 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32982
33397
|
* successful thread removal. Body carries thread_id + turn_count of
|
|
32983
33398
|
* the deleted bundle.
|
|
32984
33399
|
*/
|
|
32985
|
-
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
33400
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
33401
|
+
/**
|
|
33402
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
33403
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
33404
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
33405
|
+
* carries thread_id + a stable failure_reason enum.
|
|
33406
|
+
*/
|
|
33407
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
|
|
32986
33408
|
};
|
|
32987
33409
|
}
|
|
32988
33410
|
});
|
|
@@ -32995,13 +33417,20 @@ var init_operator_chat_types = __esm({
|
|
|
32995
33417
|
CONCIERGE_THREAD_KEY = "_fortress";
|
|
32996
33418
|
}
|
|
32997
33419
|
});
|
|
33420
|
+
function approxTokenLen(text) {
|
|
33421
|
+
return Math.ceil(text.length / 4);
|
|
33422
|
+
}
|
|
32998
33423
|
function makeEventId(prefix) {
|
|
32999
33424
|
return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
33000
33425
|
}
|
|
33426
|
+
function formatPriorTurnLine(turn) {
|
|
33427
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
33428
|
+
return `${label}: ${turn.content}`;
|
|
33429
|
+
}
|
|
33001
33430
|
function hashOf(input) {
|
|
33002
33431
|
return hashToString(sha256.sha256(stringToBytes(input)));
|
|
33003
33432
|
}
|
|
33004
|
-
var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
33433
|
+
var DEFAULT_CONCIERGE_MAX_TOKENS, DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS, DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS, DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET, DEFAULT_CONCIERGE_SESSION_TTL_MS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
33005
33434
|
var init_operator_chat_service = __esm({
|
|
33006
33435
|
"src/chat/operator-chat-service.ts"() {
|
|
33007
33436
|
init_hashing();
|
|
@@ -33009,6 +33438,10 @@ var init_operator_chat_service = __esm({
|
|
|
33009
33438
|
init_operator_chat_audit_events();
|
|
33010
33439
|
init_operator_chat_types();
|
|
33011
33440
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
33441
|
+
DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
33442
|
+
DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
33443
|
+
DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
33444
|
+
DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
33012
33445
|
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
33013
33446
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
33014
33447
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -33045,6 +33478,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33045
33478
|
piiFilter;
|
|
33046
33479
|
conciergeMaxTokens;
|
|
33047
33480
|
memory;
|
|
33481
|
+
historyWindowTurns;
|
|
33482
|
+
historyFreshnessMs;
|
|
33483
|
+
historyTokenBudget;
|
|
33484
|
+
sessionTtlMs;
|
|
33485
|
+
clock;
|
|
33048
33486
|
/**
|
|
33049
33487
|
* In-memory thread_id assigned to the active concierge session.
|
|
33050
33488
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -33052,6 +33490,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33052
33490
|
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33053
33491
|
*/
|
|
33054
33492
|
activeMemoryThreadId;
|
|
33493
|
+
/**
|
|
33494
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
33495
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
33496
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
33497
|
+
* allocates a new thread_id even though the prior one is still
|
|
33498
|
+
* readable from the memory store.
|
|
33499
|
+
*/
|
|
33500
|
+
lastInteractionAt;
|
|
33055
33501
|
constructor(deps) {
|
|
33056
33502
|
this.store = deps.store;
|
|
33057
33503
|
this.auditLog = deps.auditLog;
|
|
@@ -33063,6 +33509,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33063
33509
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
33064
33510
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33065
33511
|
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
33512
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
33513
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
33514
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
33515
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
33516
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
33066
33517
|
}
|
|
33067
33518
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
33068
33519
|
/**
|
|
@@ -33081,6 +33532,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33081
33532
|
throw new Error("concierge query must not be empty");
|
|
33082
33533
|
}
|
|
33083
33534
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
33535
|
+
const nowMs = this.clock();
|
|
33536
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
33537
|
+
this.activeMemoryThreadId = void 0;
|
|
33538
|
+
}
|
|
33084
33539
|
const operatorMessage = {
|
|
33085
33540
|
message_id: crypto.randomUUID(),
|
|
33086
33541
|
surface: "concierge",
|
|
@@ -33093,6 +33548,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33093
33548
|
CONCIERGE_THREAD_KEY,
|
|
33094
33549
|
operatorMessage
|
|
33095
33550
|
);
|
|
33551
|
+
let priorTurns = [];
|
|
33552
|
+
let memoryReadFailureReason = null;
|
|
33553
|
+
let activeThreadIdForRound;
|
|
33554
|
+
if (this.memory) {
|
|
33555
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
33556
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
33557
|
+
if (result.ok) {
|
|
33558
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
33559
|
+
const fresh = result.turns.filter((t) => {
|
|
33560
|
+
const ts = Date.parse(t.created_at);
|
|
33561
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
33562
|
+
});
|
|
33563
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
33564
|
+
priorTurns = recent;
|
|
33565
|
+
} else {
|
|
33566
|
+
memoryReadFailureReason = result.reason;
|
|
33567
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
33568
|
+
}
|
|
33569
|
+
}
|
|
33096
33570
|
if (this.memory) {
|
|
33097
33571
|
const threadId = this.ensureActiveMemoryThread();
|
|
33098
33572
|
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
@@ -33114,7 +33588,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33114
33588
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
33115
33589
|
outcome = "substrate_disabled";
|
|
33116
33590
|
} else {
|
|
33117
|
-
const context = await this.assembleConciergeContext();
|
|
33591
|
+
const context = await this.assembleConciergeContext(priorTurns);
|
|
33118
33592
|
const response = await this.substrateSelector.invokeSummarize(
|
|
33119
33593
|
"concierge",
|
|
33120
33594
|
{
|
|
@@ -33153,10 +33627,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33153
33627
|
CONCIERGE_THREAD_KEY,
|
|
33154
33628
|
responseMessage
|
|
33155
33629
|
);
|
|
33630
|
+
let assistantTurnId;
|
|
33156
33631
|
if (this.memory) {
|
|
33157
33632
|
const threadId = this.ensureActiveMemoryThread();
|
|
33158
|
-
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() =>
|
|
33159
|
-
|
|
33633
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
33634
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
33635
|
+
}
|
|
33636
|
+
if (this.memory && activeThreadIdForRound) {
|
|
33637
|
+
this.lastInteractionAt = nowMs;
|
|
33160
33638
|
}
|
|
33161
33639
|
const payload = {
|
|
33162
33640
|
version: "1.2",
|
|
@@ -33169,7 +33647,12 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33169
33647
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
33170
33648
|
substrate: servedBy,
|
|
33171
33649
|
latency_ms: latencyMs,
|
|
33172
|
-
outcome
|
|
33650
|
+
outcome,
|
|
33651
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
33652
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
33653
|
+
...this.memory ? {
|
|
33654
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
33655
|
+
} : {}
|
|
33173
33656
|
};
|
|
33174
33657
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
33175
33658
|
return {
|
|
@@ -33179,6 +33662,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33179
33662
|
outcome
|
|
33180
33663
|
};
|
|
33181
33664
|
}
|
|
33665
|
+
/**
|
|
33666
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
33667
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
33668
|
+
* with `result: "failure"` since the concierge fell back to
|
|
33669
|
+
* single-turn mode for this round-trip.
|
|
33670
|
+
*/
|
|
33671
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
33672
|
+
const payload = {
|
|
33673
|
+
version: "1.2",
|
|
33674
|
+
event_id: makeEventId("conc-memfail"),
|
|
33675
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33676
|
+
identity_id: this.identityId,
|
|
33677
|
+
kind: "operator_concierge_memory_read_failed",
|
|
33678
|
+
surface: "concierge",
|
|
33679
|
+
thread_id: threadId,
|
|
33680
|
+
failure_reason: reason
|
|
33681
|
+
};
|
|
33682
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
33683
|
+
}
|
|
33182
33684
|
/**
|
|
33183
33685
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
33184
33686
|
* an empty array when no thread exists yet.
|
|
@@ -33301,6 +33803,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33301
33803
|
* ## Sanctuary reference
|
|
33302
33804
|
* <static domain reference block>
|
|
33303
33805
|
*
|
|
33806
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
33807
|
+
* OPERATOR: ...
|
|
33808
|
+
* CONCIERGE: ...
|
|
33809
|
+
* ---
|
|
33810
|
+
*
|
|
33304
33811
|
* ## Recent activity
|
|
33305
33812
|
* <recentActivity output>
|
|
33306
33813
|
*
|
|
@@ -33310,37 +33817,69 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33310
33817
|
* ## Open inbox
|
|
33311
33818
|
* <openInbox output>
|
|
33312
33819
|
* ```
|
|
33313
|
-
|
|
33314
|
-
|
|
33820
|
+
*
|
|
33821
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
33822
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
33823
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
33824
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
33825
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
33826
|
+
* serialization is the canonical path for v1.3.
|
|
33827
|
+
*/
|
|
33828
|
+
async assembleConciergeContext(priorTurns = []) {
|
|
33315
33829
|
const ref = `## Sanctuary reference
|
|
33316
33830
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
33831
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
33317
33832
|
if (!this.contextProviders) {
|
|
33318
|
-
return
|
|
33319
|
-
|
|
33320
|
-
|
|
33321
|
-
(no providers wired)
|
|
33322
|
-
|
|
33323
|
-
##
|
|
33324
|
-
(
|
|
33325
|
-
|
|
33326
|
-
## Open inbox
|
|
33327
|
-
(no providers wired)`;
|
|
33833
|
+
return [
|
|
33834
|
+
ref,
|
|
33835
|
+
...priorSection ? [priorSection] : [],
|
|
33836
|
+
"## Recent activity\n(no providers wired)",
|
|
33837
|
+
"## Wrapped agents\n(no providers wired)",
|
|
33838
|
+
"## Open inbox\n(no providers wired)"
|
|
33839
|
+
].join("\n\n");
|
|
33328
33840
|
}
|
|
33329
33841
|
const [activity, agents, inbox] = await Promise.all([
|
|
33330
33842
|
this.contextProviders.recentActivity(),
|
|
33331
33843
|
this.contextProviders.agentInventory(),
|
|
33332
33844
|
this.contextProviders.openInbox()
|
|
33333
33845
|
]);
|
|
33334
|
-
return
|
|
33335
|
-
|
|
33336
|
-
|
|
33337
|
-
|
|
33338
|
-
|
|
33339
|
-
|
|
33340
|
-
${agents}
|
|
33341
|
-
|
|
33342
|
-
|
|
33343
|
-
|
|
33846
|
+
return [
|
|
33847
|
+
ref,
|
|
33848
|
+
...priorSection ? [priorSection] : [],
|
|
33849
|
+
`## Recent activity
|
|
33850
|
+
${activity}`,
|
|
33851
|
+
`## Wrapped agents
|
|
33852
|
+
${agents}`,
|
|
33853
|
+
`## Open inbox
|
|
33854
|
+
${inbox}`
|
|
33855
|
+
].join("\n\n");
|
|
33856
|
+
}
|
|
33857
|
+
/**
|
|
33858
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
33859
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
33860
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
33861
|
+
* the input is empty or when the budget excludes every turn.
|
|
33862
|
+
*/
|
|
33863
|
+
formatPriorTurnsSection(turns) {
|
|
33864
|
+
if (turns.length === 0) return "";
|
|
33865
|
+
const HEADER = "## Prior conversation";
|
|
33866
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
33867
|
+
const headerTokens = approxTokenLen(`${HEADER}
|
|
33868
|
+
`);
|
|
33869
|
+
const sepTokens = approxTokenLen("\n");
|
|
33870
|
+
let runningTokens = headerTokens;
|
|
33871
|
+
let runningLines = [];
|
|
33872
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
33873
|
+
const line = lines[i];
|
|
33874
|
+
const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
33875
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
33876
|
+
runningTokens += tokens;
|
|
33877
|
+
runningLines.push(line);
|
|
33878
|
+
}
|
|
33879
|
+
if (runningLines.length === 0) return "";
|
|
33880
|
+
runningLines = runningLines.reverse();
|
|
33881
|
+
return `${HEADER}
|
|
33882
|
+
${runningLines.join("\n")}`;
|
|
33344
33883
|
}
|
|
33345
33884
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
33346
33885
|
emit(operation, payload, result) {
|
|
@@ -33545,6 +34084,65 @@ var init_concierge_memory_store = __esm({
|
|
|
33545
34084
|
}
|
|
33546
34085
|
return turns;
|
|
33547
34086
|
}
|
|
34087
|
+
/**
|
|
34088
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
34089
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
34090
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
34091
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
34092
|
+
* with a concrete cause.
|
|
34093
|
+
*
|
|
34094
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
34095
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
34096
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
34097
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
34098
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
34099
|
+
* - Storage IO error → `io_failed`.
|
|
34100
|
+
*/
|
|
34101
|
+
async readThreadStrict(threadId, opts) {
|
|
34102
|
+
const key = bundleKey(threadId);
|
|
34103
|
+
let raw;
|
|
34104
|
+
try {
|
|
34105
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
34106
|
+
} catch {
|
|
34107
|
+
return { ok: false, reason: "io_failed" };
|
|
34108
|
+
}
|
|
34109
|
+
if (!raw) return { ok: true, turns: [] };
|
|
34110
|
+
if (raw.length > MAX_BUNDLE_BYTES2) {
|
|
34111
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
34112
|
+
}
|
|
34113
|
+
let envelope;
|
|
34114
|
+
try {
|
|
34115
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
34116
|
+
} catch {
|
|
34117
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34118
|
+
}
|
|
34119
|
+
let plaintext;
|
|
34120
|
+
try {
|
|
34121
|
+
const aad = stringToBytes(threadId);
|
|
34122
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
34123
|
+
} catch {
|
|
34124
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
34125
|
+
}
|
|
34126
|
+
let parsed;
|
|
34127
|
+
try {
|
|
34128
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
34129
|
+
} catch {
|
|
34130
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34131
|
+
}
|
|
34132
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
34133
|
+
if (parsed.thread_id !== threadId) {
|
|
34134
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34135
|
+
}
|
|
34136
|
+
let turns = parsed.turns;
|
|
34137
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
34138
|
+
const cutoff = opts.sinceTurnId;
|
|
34139
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
34140
|
+
}
|
|
34141
|
+
if (opts?.limit !== void 0) {
|
|
34142
|
+
turns = turns.slice(0, opts.limit);
|
|
34143
|
+
}
|
|
34144
|
+
return { ok: true, turns };
|
|
34145
|
+
}
|
|
33548
34146
|
/**
|
|
33549
34147
|
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33550
34148
|
* Sorted newest-first by last_turn_at.
|
|
@@ -36442,7 +37040,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
36442
37040
|
}
|
|
36443
37041
|
return null;
|
|
36444
37042
|
}
|
|
36445
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
37043
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
36446
37044
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
36447
37045
|
if (!destinationSigner) {
|
|
36448
37046
|
return {
|
|
@@ -36504,8 +37102,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36504
37102
|
}
|
|
36505
37103
|
}
|
|
36506
37104
|
}
|
|
37105
|
+
let plaintext;
|
|
36507
37106
|
try {
|
|
36508
|
-
|
|
37107
|
+
plaintext = decrypt(
|
|
36509
37108
|
item.entry.payload,
|
|
36510
37109
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
36511
37110
|
);
|
|
@@ -36514,28 +37113,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36514
37113
|
skipped++;
|
|
36515
37114
|
continue;
|
|
36516
37115
|
}
|
|
36517
|
-
await stateStore.write(
|
|
36518
|
-
item.namespace,
|
|
36519
|
-
item.key,
|
|
36520
|
-
bytesToString(plaintext),
|
|
36521
|
-
destinationSigner.identity_id,
|
|
36522
|
-
destinationSigner.encrypted_private_key,
|
|
36523
|
-
identityEncryptionKey,
|
|
36524
|
-
{
|
|
36525
|
-
content_type: item.entry.metadata.content_type,
|
|
36526
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
36527
|
-
tags: [
|
|
36528
|
-
...item.entry.metadata.tags ?? [],
|
|
36529
|
-
"exit-import",
|
|
36530
|
-
`source:${item.entry.kid}`
|
|
36531
|
-
]
|
|
36532
|
-
}
|
|
36533
|
-
);
|
|
36534
|
-
imported++;
|
|
36535
37116
|
} catch {
|
|
36536
37117
|
skippedInvalidSig++;
|
|
36537
37118
|
skipped++;
|
|
37119
|
+
continue;
|
|
36538
37120
|
}
|
|
37121
|
+
await stateStore.write(
|
|
37122
|
+
item.namespace,
|
|
37123
|
+
item.key,
|
|
37124
|
+
bytesToString(plaintext),
|
|
37125
|
+
destinationSigner.identity_id,
|
|
37126
|
+
destinationSigner.encrypted_private_key,
|
|
37127
|
+
identityEncryptionKey,
|
|
37128
|
+
{
|
|
37129
|
+
content_type: item.entry.metadata.content_type,
|
|
37130
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
37131
|
+
tags: [
|
|
37132
|
+
...item.entry.metadata.tags ?? [],
|
|
37133
|
+
"exit-import",
|
|
37134
|
+
`source:${item.entry.kid}`
|
|
37135
|
+
]
|
|
37136
|
+
}
|
|
37137
|
+
);
|
|
37138
|
+
imported++;
|
|
37139
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
36539
37140
|
}
|
|
36540
37141
|
return {
|
|
36541
37142
|
status: "rekeyed",
|
|
@@ -36546,6 +37147,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36546
37147
|
conflicts
|
|
36547
37148
|
};
|
|
36548
37149
|
}
|
|
37150
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
37151
|
+
let removed = 0;
|
|
37152
|
+
const failed = [];
|
|
37153
|
+
for (const loc of staged) {
|
|
37154
|
+
try {
|
|
37155
|
+
const ok2 = await storage.delete(loc.namespace, loc.key);
|
|
37156
|
+
if (ok2) {
|
|
37157
|
+
removed++;
|
|
37158
|
+
} else {
|
|
37159
|
+
failed.push(loc);
|
|
37160
|
+
}
|
|
37161
|
+
} catch {
|
|
37162
|
+
failed.push(loc);
|
|
37163
|
+
}
|
|
37164
|
+
}
|
|
37165
|
+
return { removed, failed };
|
|
37166
|
+
}
|
|
36549
37167
|
async function stageArtifact(storage, namespace, key, value) {
|
|
36550
37168
|
await storage.write(namespace, key, jsonBytes(value));
|
|
36551
37169
|
}
|
|
@@ -36670,6 +37288,8 @@ async function importExitBundle(opts) {
|
|
|
36670
37288
|
}
|
|
36671
37289
|
const importId = importIdForManifest(manifest);
|
|
36672
37290
|
const stagedArtifacts = [];
|
|
37291
|
+
const stagedLocations = [];
|
|
37292
|
+
const importedRekeyEntries = [];
|
|
36673
37293
|
if (identityArtifact) {
|
|
36674
37294
|
await stageArtifact(
|
|
36675
37295
|
opts.storage,
|
|
@@ -36678,10 +37298,15 @@ async function importExitBundle(opts) {
|
|
|
36678
37298
|
identityArtifact.json
|
|
36679
37299
|
);
|
|
36680
37300
|
stagedArtifacts.push("public_identity");
|
|
37301
|
+
stagedLocations.push({
|
|
37302
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
37303
|
+
key: identityArtifact.json.bundle.identity_id
|
|
37304
|
+
});
|
|
36681
37305
|
}
|
|
36682
37306
|
if (policySet) {
|
|
36683
37307
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
36684
37308
|
stagedArtifacts.push("policy_set");
|
|
37309
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
36685
37310
|
}
|
|
36686
37311
|
if (auditReceipts) {
|
|
36687
37312
|
await stageArtifact(
|
|
@@ -36691,10 +37316,12 @@ async function importExitBundle(opts) {
|
|
|
36691
37316
|
auditReceipts.json
|
|
36692
37317
|
);
|
|
36693
37318
|
stagedArtifacts.push("audit_receipts");
|
|
37319
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
36694
37320
|
}
|
|
36695
37321
|
if (commitments) {
|
|
36696
37322
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
36697
37323
|
stagedArtifacts.push("commitments");
|
|
37324
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
36698
37325
|
}
|
|
36699
37326
|
if (placeholderMetadata) {
|
|
36700
37327
|
await stageArtifact(
|
|
@@ -36704,12 +37331,17 @@ async function importExitBundle(opts) {
|
|
|
36704
37331
|
placeholderMetadata.json
|
|
36705
37332
|
);
|
|
36706
37333
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
37334
|
+
stagedLocations.push({
|
|
37335
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
37336
|
+
key: importId
|
|
37337
|
+
});
|
|
36707
37338
|
}
|
|
36708
37339
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
36709
37340
|
manifest: manifest.body,
|
|
36710
37341
|
verified_at: verification.verified_at,
|
|
36711
37342
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
36712
37343
|
});
|
|
37344
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
36713
37345
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
36714
37346
|
let reputationResult = {
|
|
36715
37347
|
imported_attestations: 0,
|
|
@@ -36734,26 +37366,57 @@ async function importExitBundle(opts) {
|
|
|
36734
37366
|
encryptedState?.json ?? null,
|
|
36735
37367
|
opts
|
|
36736
37368
|
);
|
|
36737
|
-
|
|
36738
|
-
|
|
36739
|
-
|
|
36740
|
-
|
|
36741
|
-
|
|
36742
|
-
|
|
36743
|
-
|
|
36744
|
-
|
|
36745
|
-
|
|
36746
|
-
|
|
36747
|
-
|
|
36748
|
-
|
|
36749
|
-
|
|
36750
|
-
|
|
36751
|
-
|
|
36752
|
-
|
|
36753
|
-
|
|
36754
|
-
|
|
36755
|
-
|
|
36756
|
-
|
|
37369
|
+
let stateResult;
|
|
37370
|
+
try {
|
|
37371
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
37372
|
+
encryptedState.json,
|
|
37373
|
+
opts,
|
|
37374
|
+
sourceMasterKey,
|
|
37375
|
+
publicKeys.byIdentityId,
|
|
37376
|
+
importedRekeyEntries
|
|
37377
|
+
) : {
|
|
37378
|
+
status: "staged_requires_source_key",
|
|
37379
|
+
imported_keys: 0,
|
|
37380
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
37381
|
+
skipped_invalid_sig: 0,
|
|
37382
|
+
skipped_unknown_kid: 0,
|
|
37383
|
+
conflicts: conflicts.state_conflicts.length
|
|
37384
|
+
} : {
|
|
37385
|
+
status: "not_requested",
|
|
37386
|
+
imported_keys: 0,
|
|
37387
|
+
skipped_keys: 0,
|
|
37388
|
+
skipped_invalid_sig: 0,
|
|
37389
|
+
skipped_unknown_kid: 0,
|
|
37390
|
+
conflicts: 0
|
|
37391
|
+
};
|
|
37392
|
+
} catch (err) {
|
|
37393
|
+
const toCleanup = [
|
|
37394
|
+
...importedRekeyEntries,
|
|
37395
|
+
...stagedLocations
|
|
37396
|
+
];
|
|
37397
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
37398
|
+
opts.auditLog.append(
|
|
37399
|
+
"l1",
|
|
37400
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
37401
|
+
manifest.body.identity_binding.identity_id,
|
|
37402
|
+
{
|
|
37403
|
+
import_id: importId,
|
|
37404
|
+
manifest_version: manifest.body.manifest_version,
|
|
37405
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
37406
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
37407
|
+
removed_total: cleanup.removed,
|
|
37408
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
37409
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
37410
|
+
},
|
|
37411
|
+
"failure"
|
|
37412
|
+
);
|
|
37413
|
+
await opts.auditLog.flush();
|
|
37414
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
37415
|
+
throw new ExitBundleImportError(
|
|
37416
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
37417
|
+
`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).`
|
|
37418
|
+
);
|
|
37419
|
+
}
|
|
36757
37420
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
36758
37421
|
import_id: importId,
|
|
36759
37422
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -37832,7 +38495,6 @@ ${err.message}
|
|
|
37832
38495
|
timestamp: alert.timestamp
|
|
37833
38496
|
});
|
|
37834
38497
|
} : void 0;
|
|
37835
|
-
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37836
38498
|
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37837
38499
|
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37838
38500
|
const approvalAggregator = new ApprovalAggregator({
|
|
@@ -37842,6 +38504,20 @@ ${err.message}
|
|
|
37842
38504
|
identityId: aggregatorIdentityId,
|
|
37843
38505
|
fortressId: fortressIdForAggregator
|
|
37844
38506
|
});
|
|
38507
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
38508
|
+
underlying: approvalChannel,
|
|
38509
|
+
aggregator: approvalAggregator,
|
|
38510
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
38511
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
38512
|
+
});
|
|
38513
|
+
const gate = new ApprovalGate(
|
|
38514
|
+
policy,
|
|
38515
|
+
baseline,
|
|
38516
|
+
wrappedApprovalChannel,
|
|
38517
|
+
auditLog,
|
|
38518
|
+
injectionDetector,
|
|
38519
|
+
onInjectionAlert
|
|
38520
|
+
);
|
|
37845
38521
|
gate.setApprovalEventCallback((event) => {
|
|
37846
38522
|
void approvalAggregator.ingest(event);
|
|
37847
38523
|
});
|
|
@@ -38039,6 +38715,7 @@ var init_src = __esm({
|
|
|
38039
38715
|
init_webhook();
|
|
38040
38716
|
init_gate();
|
|
38041
38717
|
init_approval_aggregator();
|
|
38718
|
+
init_aggregator_backed_channel();
|
|
38042
38719
|
init_tools4();
|
|
38043
38720
|
init_router();
|
|
38044
38721
|
init_router();
|
|
@@ -41003,6 +41680,22 @@ var init_broker = __esm({
|
|
|
41003
41680
|
auditLog;
|
|
41004
41681
|
issuer;
|
|
41005
41682
|
principalIdentityId;
|
|
41683
|
+
/**
|
|
41684
|
+
* Per-secret-name mutex. Hardening wave 6 finding #64: two concurrent
|
|
41685
|
+
* addSecret() / rotateSecret() / deleteSecret() calls on the same name
|
|
41686
|
+
* MUST serialize cleanly. The keychain backend's `find-then-add` and
|
|
41687
|
+
* `find-then-delete-then-add` shapes (KeychainBackend.addSecret /
|
|
41688
|
+
* .rotateSecret) are not atomic against another caller racing the same
|
|
41689
|
+
* service-name; without serialization the second caller can observe a
|
|
41690
|
+
* stale "exists" check and either drop the new value or leave a
|
|
41691
|
+
* duplicate keychain entry.
|
|
41692
|
+
*
|
|
41693
|
+
* Implementation: an in-memory promise chain per name. Subsequent
|
|
41694
|
+
* callers `await` the chain tail and append their own work; failures
|
|
41695
|
+
* propagate to the failing caller without poisoning the chain for
|
|
41696
|
+
* later callers.
|
|
41697
|
+
*/
|
|
41698
|
+
nameLocks = /* @__PURE__ */ new Map();
|
|
41006
41699
|
constructor(opts) {
|
|
41007
41700
|
this.backend = opts.backend;
|
|
41008
41701
|
this.auditLog = opts.auditLog;
|
|
@@ -41013,6 +41706,40 @@ var init_broker = __esm({
|
|
|
41013
41706
|
grants: opts.grants
|
|
41014
41707
|
});
|
|
41015
41708
|
}
|
|
41709
|
+
/**
|
|
41710
|
+
* Serialize `op` against any other in-flight write to the same secret
|
|
41711
|
+
* `name`. Per-name fairness only, distinct names run in parallel.
|
|
41712
|
+
* The current chain tail is used as the acceptance gate; we then
|
|
41713
|
+
* publish a new tail that swallows the operation's outcome so a
|
|
41714
|
+
* thrown error does not poison the next caller's wait.
|
|
41715
|
+
*/
|
|
41716
|
+
async withNameLock(name, op) {
|
|
41717
|
+
const previous = this.nameLocks.get(name) ?? Promise.resolve();
|
|
41718
|
+
let release = () => {
|
|
41719
|
+
};
|
|
41720
|
+
const next = new Promise((resolve8) => {
|
|
41721
|
+
release = resolve8;
|
|
41722
|
+
});
|
|
41723
|
+
this.nameLocks.set(name, next);
|
|
41724
|
+
try {
|
|
41725
|
+
await previous.catch(() => {
|
|
41726
|
+
});
|
|
41727
|
+
return await op();
|
|
41728
|
+
} finally {
|
|
41729
|
+
release();
|
|
41730
|
+
if (this.nameLocks.get(name) === next) {
|
|
41731
|
+
this.nameLocks.delete(name);
|
|
41732
|
+
}
|
|
41733
|
+
}
|
|
41734
|
+
}
|
|
41735
|
+
/**
|
|
41736
|
+
* Diagnostic-only: visible for tests so they can assert that distinct
|
|
41737
|
+
* names do not contend on a shared lock. Not part of the public broker
|
|
41738
|
+
* contract; do not consume from production code.
|
|
41739
|
+
*/
|
|
41740
|
+
__nameLockCountForTests() {
|
|
41741
|
+
return this.nameLocks.size;
|
|
41742
|
+
}
|
|
41016
41743
|
/** Ensure backend is initialized and unlocked. Audits the unlock. */
|
|
41017
41744
|
async ensureUnlocked(passphrase) {
|
|
41018
41745
|
await this.backend.ensureInitialized(passphrase);
|
|
@@ -41025,31 +41752,37 @@ var init_broker = __esm({
|
|
|
41025
41752
|
);
|
|
41026
41753
|
}
|
|
41027
41754
|
async addSecret(name, value) {
|
|
41028
|
-
await this.
|
|
41029
|
-
|
|
41030
|
-
|
|
41031
|
-
|
|
41032
|
-
|
|
41033
|
-
|
|
41034
|
-
|
|
41755
|
+
await this.withNameLock(name, async () => {
|
|
41756
|
+
await this.backend.addSecret(name, value);
|
|
41757
|
+
this.auditLog.append(
|
|
41758
|
+
"l3",
|
|
41759
|
+
BROKER_OPS.SECRET_ADDED,
|
|
41760
|
+
this.principalIdentityId,
|
|
41761
|
+
{ secret: name }
|
|
41762
|
+
);
|
|
41763
|
+
});
|
|
41035
41764
|
}
|
|
41036
41765
|
async rotateSecret(name, newValue) {
|
|
41037
|
-
await this.
|
|
41038
|
-
|
|
41039
|
-
|
|
41040
|
-
|
|
41041
|
-
|
|
41042
|
-
|
|
41043
|
-
|
|
41766
|
+
await this.withNameLock(name, async () => {
|
|
41767
|
+
await this.backend.rotateSecret(name, newValue);
|
|
41768
|
+
this.auditLog.append(
|
|
41769
|
+
"l3",
|
|
41770
|
+
BROKER_OPS.SECRET_ROTATED,
|
|
41771
|
+
this.principalIdentityId,
|
|
41772
|
+
{ secret: name }
|
|
41773
|
+
);
|
|
41774
|
+
});
|
|
41044
41775
|
}
|
|
41045
41776
|
async deleteSecret(name) {
|
|
41046
|
-
await this.
|
|
41047
|
-
|
|
41048
|
-
|
|
41049
|
-
|
|
41050
|
-
|
|
41051
|
-
|
|
41052
|
-
|
|
41777
|
+
await this.withNameLock(name, async () => {
|
|
41778
|
+
await this.backend.deleteSecret(name);
|
|
41779
|
+
this.auditLog.append(
|
|
41780
|
+
"l3",
|
|
41781
|
+
BROKER_OPS.SECRET_DELETED,
|
|
41782
|
+
this.principalIdentityId,
|
|
41783
|
+
{ secret: name }
|
|
41784
|
+
);
|
|
41785
|
+
});
|
|
41053
41786
|
}
|
|
41054
41787
|
async listSecretNames() {
|
|
41055
41788
|
return this.backend.listSecretNames();
|
|
@@ -41089,6 +41822,19 @@ var init_broker = __esm({
|
|
|
41089
41822
|
liveTokenCount() {
|
|
41090
41823
|
return this.issuer.liveTokenCount();
|
|
41091
41824
|
}
|
|
41825
|
+
/**
|
|
41826
|
+
* Drop expired tokens from the in-memory issuer map. Hardening wave 6
|
|
41827
|
+
* finding #86: previously expiry pruning depended on opportunistic
|
|
41828
|
+
* `pruneExpired()` calls; now the cocoon-unlock initialization path
|
|
41829
|
+
* (openBroker -> after backend.ensureInitialized -> after Broker
|
|
41830
|
+
* construction) fires this once so each cocoon-unlock cycle drops
|
|
41831
|
+
* stale bindings before any operator interaction.
|
|
41832
|
+
*
|
|
41833
|
+
* Returns the number of tokens removed. Safe to call repeatedly; idempotent.
|
|
41834
|
+
*/
|
|
41835
|
+
pruneExpiredTokens() {
|
|
41836
|
+
return this.issuer.pruneExpired();
|
|
41837
|
+
}
|
|
41092
41838
|
/**
|
|
41093
41839
|
* Audit query restricted to broker-scoped operations. Returns entries
|
|
41094
41840
|
* with their timestamps, op, and result (never the secret value).
|
|
@@ -41247,6 +41993,7 @@ async function openBroker(opts = {}) {
|
|
|
41247
41993
|
grants,
|
|
41248
41994
|
principalIdentityId: opts.principalIdentityId ?? "sanctuary-broker"
|
|
41249
41995
|
});
|
|
41996
|
+
broker.pruneExpiredTokens();
|
|
41250
41997
|
return {
|
|
41251
41998
|
broker,
|
|
41252
41999
|
close: async () => {
|
|
@@ -42115,8 +42862,6 @@ var init_health = __esm({
|
|
|
42115
42862
|
DEFAULT_TIMEOUT_MS4 = 500;
|
|
42116
42863
|
}
|
|
42117
42864
|
});
|
|
42118
|
-
|
|
42119
|
-
// src/cli/agents/cli.ts
|
|
42120
42865
|
function resolveCtx(args) {
|
|
42121
42866
|
const env = args.env ?? process.env;
|
|
42122
42867
|
const discoverOpts = {
|
|
@@ -42154,6 +42899,8 @@ async function runAgentsCommand(args) {
|
|
|
42154
42899
|
return await cmdShow2(rest, ctx);
|
|
42155
42900
|
case "status":
|
|
42156
42901
|
return await cmdStatus(rest, ctx);
|
|
42902
|
+
case "config":
|
|
42903
|
+
return await cmdConfig(rest, ctx);
|
|
42157
42904
|
default:
|
|
42158
42905
|
ctx.err.write(`Unknown subcommand: ${sub}
|
|
42159
42906
|
`);
|
|
@@ -42169,10 +42916,18 @@ async function runAgentsCommand(args) {
|
|
|
42169
42916
|
}
|
|
42170
42917
|
function printUsage4(s) {
|
|
42171
42918
|
s.write(`Usage: sanctuary agents <command> [flags]
|
|
42919
|
+
sanctuary agent <command> [flags] (alias)
|
|
42172
42920
|
|
|
42173
42921
|
list [--json] List every tenant visible on this host.
|
|
42174
|
-
show <tenant> [--json] Show details for one tenant
|
|
42922
|
+
show <tenant> [--json] Show details for one tenant (includes
|
|
42923
|
+
approval-redirect state).
|
|
42175
42924
|
status [--json] One-line-per-tenant running/stopped summary.
|
|
42925
|
+
config <tenant> [opts] Write tenant principal-policy.yaml fields.
|
|
42926
|
+
--approval-redirect=<bool> Toggle cross-harness inbox redirect.
|
|
42927
|
+
--approval-redirect-mode=<replace|notify>
|
|
42928
|
+
Pick replace (bypass underlying channel)
|
|
42929
|
+
or notify (race both paths). Default
|
|
42930
|
+
replace when toggled on.
|
|
42176
42931
|
|
|
42177
42932
|
Options:
|
|
42178
42933
|
--fortress <path> Scope discovery to a specific storage path
|
|
@@ -42274,6 +43029,7 @@ async function cmdShow2(argv, ctx) {
|
|
|
42274
43029
|
return 1;
|
|
42275
43030
|
}
|
|
42276
43031
|
const probe = await ctx.probe(tenant);
|
|
43032
|
+
const approvalRedirect = await readApprovalRedirectState(tenant);
|
|
42277
43033
|
const payload = {
|
|
42278
43034
|
name: tenant.name,
|
|
42279
43035
|
storage_path: tenant.storage_path,
|
|
@@ -42287,7 +43043,8 @@ async function cmdShow2(argv, ctx) {
|
|
|
42287
43043
|
running: probe.running,
|
|
42288
43044
|
status: probe.status,
|
|
42289
43045
|
reason: probe.reason
|
|
42290
|
-
}
|
|
43046
|
+
},
|
|
43047
|
+
approval_redirect: approvalRedirect
|
|
42291
43048
|
};
|
|
42292
43049
|
if (hasJsonFlag(argv)) {
|
|
42293
43050
|
ctx.out.write(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -42331,10 +43088,173 @@ async function cmdShow2(argv, ctx) {
|
|
|
42331
43088
|
}
|
|
42332
43089
|
ctx.out.write(
|
|
42333
43090
|
`probe: ${probe.running ? "running" : "not-running"}${probe.reason ? ` (${probe.reason})` : ""}
|
|
43091
|
+
`
|
|
43092
|
+
);
|
|
43093
|
+
ctx.out.write(
|
|
43094
|
+
`approval_redirect: ${approvalRedirect.enabled ? `on (${approvalRedirect.mode})` : "off"}
|
|
42334
43095
|
`
|
|
42335
43096
|
);
|
|
42336
43097
|
return 0;
|
|
42337
43098
|
}
|
|
43099
|
+
async function readApprovalRedirectState(tenant) {
|
|
43100
|
+
const policyPath = path.join(tenant.storage_path, "principal-policy.yaml");
|
|
43101
|
+
try {
|
|
43102
|
+
const content = await promises.readFile(policyPath, "utf-8");
|
|
43103
|
+
const parsed = parsePolicy(content);
|
|
43104
|
+
const cfg = parsed.approval_redirect;
|
|
43105
|
+
if (!cfg) return { enabled: false, mode: "replace" };
|
|
43106
|
+
return {
|
|
43107
|
+
enabled: !!cfg.enabled,
|
|
43108
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
43109
|
+
};
|
|
43110
|
+
} catch {
|
|
43111
|
+
return { enabled: false, mode: "replace" };
|
|
43112
|
+
}
|
|
43113
|
+
}
|
|
43114
|
+
function parseBoolFlag(raw) {
|
|
43115
|
+
if (raw === void 0) return null;
|
|
43116
|
+
const v = raw.toLowerCase();
|
|
43117
|
+
if (v === "true" || v === "yes" || v === "on" || v === "1") return true;
|
|
43118
|
+
if (v === "false" || v === "no" || v === "off" || v === "0") return false;
|
|
43119
|
+
return null;
|
|
43120
|
+
}
|
|
43121
|
+
function findFlagValue(argv, name) {
|
|
43122
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43123
|
+
const a = argv[i];
|
|
43124
|
+
if (a === name) {
|
|
43125
|
+
return argv[i + 1];
|
|
43126
|
+
}
|
|
43127
|
+
const eq = `${name}=`;
|
|
43128
|
+
if (a.startsWith(eq)) {
|
|
43129
|
+
return a.slice(eq.length);
|
|
43130
|
+
}
|
|
43131
|
+
}
|
|
43132
|
+
return void 0;
|
|
43133
|
+
}
|
|
43134
|
+
async function cmdConfig(argv, ctx) {
|
|
43135
|
+
const positional = argv.find((a) => !a.startsWith("--"));
|
|
43136
|
+
if (!positional) {
|
|
43137
|
+
ctx.err.write(
|
|
43138
|
+
"Missing tenant. Usage: sanctuary agents config <tenant> --approval-redirect=<bool>\n"
|
|
43139
|
+
);
|
|
43140
|
+
return 2;
|
|
43141
|
+
}
|
|
43142
|
+
const tenant = await findTenant(positional, ctx.discoverOpts);
|
|
43143
|
+
if (!tenant) {
|
|
43144
|
+
ctx.err.write(`sanctuary agents: unknown tenant "${positional}"
|
|
43145
|
+
`);
|
|
43146
|
+
return 1;
|
|
43147
|
+
}
|
|
43148
|
+
const redirectFlag = parseBoolFlag(
|
|
43149
|
+
findFlagValue(argv, "--approval-redirect")
|
|
43150
|
+
);
|
|
43151
|
+
const modeFlag = findFlagValue(argv, "--approval-redirect-mode");
|
|
43152
|
+
if (redirectFlag === null && modeFlag === void 0) {
|
|
43153
|
+
ctx.err.write(
|
|
43154
|
+
"sanctuary agents config: nothing to do. Pass --approval-redirect=<bool> or --approval-redirect-mode=<replace|notify>.\n"
|
|
43155
|
+
);
|
|
43156
|
+
return 2;
|
|
43157
|
+
}
|
|
43158
|
+
if (modeFlag !== void 0 && modeFlag !== "replace" && modeFlag !== "notify") {
|
|
43159
|
+
ctx.err.write(
|
|
43160
|
+
`sanctuary agents config: --approval-redirect-mode must be "replace" or "notify" (got "${modeFlag}")
|
|
43161
|
+
`
|
|
43162
|
+
);
|
|
43163
|
+
return 2;
|
|
43164
|
+
}
|
|
43165
|
+
const current = await readApprovalRedirectState(tenant);
|
|
43166
|
+
const next = {
|
|
43167
|
+
enabled: redirectFlag !== null ? redirectFlag : current.enabled,
|
|
43168
|
+
mode: modeFlag === "notify" || modeFlag === "replace" ? modeFlag : current.mode
|
|
43169
|
+
};
|
|
43170
|
+
await writeApprovalRedirectToPolicyFile(tenant.storage_path, next);
|
|
43171
|
+
if (hasJsonFlag(argv)) {
|
|
43172
|
+
ctx.out.write(
|
|
43173
|
+
JSON.stringify(
|
|
43174
|
+
{
|
|
43175
|
+
tenant: tenant.name,
|
|
43176
|
+
approval_redirect: next
|
|
43177
|
+
},
|
|
43178
|
+
null,
|
|
43179
|
+
2
|
|
43180
|
+
) + "\n"
|
|
43181
|
+
);
|
|
43182
|
+
} else {
|
|
43183
|
+
ctx.out.write(
|
|
43184
|
+
`sanctuary agents config: tenant "${tenant.name}" approval_redirect=${next.enabled ? `on (${next.mode})` : "off"}
|
|
43185
|
+
`
|
|
43186
|
+
);
|
|
43187
|
+
ctx.out.write(
|
|
43188
|
+
` Takes effect on the next gate request for the running server.
|
|
43189
|
+
`
|
|
43190
|
+
);
|
|
43191
|
+
}
|
|
43192
|
+
return 0;
|
|
43193
|
+
}
|
|
43194
|
+
async function writeApprovalRedirectToPolicyFile(storagePath, state) {
|
|
43195
|
+
const policyPath = path.join(storagePath, "principal-policy.yaml");
|
|
43196
|
+
let content;
|
|
43197
|
+
try {
|
|
43198
|
+
content = await promises.readFile(policyPath, "utf-8");
|
|
43199
|
+
} catch (err) {
|
|
43200
|
+
const code = err?.code;
|
|
43201
|
+
if (code !== "ENOENT") throw err;
|
|
43202
|
+
content = await defaultPolicyTextForBootstrap();
|
|
43203
|
+
}
|
|
43204
|
+
const block = renderApprovalRedirectBlock(state);
|
|
43205
|
+
const updated = upsertApprovalRedirectBlock(content, block);
|
|
43206
|
+
await promises.writeFile(policyPath, updated, "utf-8");
|
|
43207
|
+
await promises.chmod(policyPath, 384);
|
|
43208
|
+
}
|
|
43209
|
+
function renderApprovalRedirectBlock(state) {
|
|
43210
|
+
return [
|
|
43211
|
+
"# Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2)",
|
|
43212
|
+
"approval_redirect:",
|
|
43213
|
+
` enabled: ${state.enabled ? "true" : "false"}`,
|
|
43214
|
+
` mode: ${state.mode}`
|
|
43215
|
+
].join("\n");
|
|
43216
|
+
}
|
|
43217
|
+
function upsertApprovalRedirectBlock(content, block) {
|
|
43218
|
+
const lines = content.split("\n");
|
|
43219
|
+
const startIdx = lines.findIndex((l) => l.startsWith("approval_redirect:"));
|
|
43220
|
+
if (startIdx === -1) {
|
|
43221
|
+
const trimmed = content.endsWith("\n") ? content : content + "\n";
|
|
43222
|
+
return trimmed + "\n" + block + "\n";
|
|
43223
|
+
}
|
|
43224
|
+
let blockStart = startIdx;
|
|
43225
|
+
if (blockStart > 0 && lines[blockStart - 1] !== void 0 && lines[blockStart - 1].startsWith("# Approval Redirect")) {
|
|
43226
|
+
blockStart = blockStart - 1;
|
|
43227
|
+
}
|
|
43228
|
+
let blockEnd = startIdx + 1;
|
|
43229
|
+
while (blockEnd < lines.length) {
|
|
43230
|
+
const l = lines[blockEnd];
|
|
43231
|
+
if (l === "") {
|
|
43232
|
+
blockEnd++;
|
|
43233
|
+
continue;
|
|
43234
|
+
}
|
|
43235
|
+
if (/^[A-Za-z0-9#]/.test(l)) {
|
|
43236
|
+
break;
|
|
43237
|
+
}
|
|
43238
|
+
blockEnd++;
|
|
43239
|
+
}
|
|
43240
|
+
const before = lines.slice(0, blockStart);
|
|
43241
|
+
const after = lines.slice(blockEnd);
|
|
43242
|
+
const replaced = [...before, ...block.split("\n"), ...after].join("\n");
|
|
43243
|
+
return replaced.endsWith("\n") ? replaced : replaced + "\n";
|
|
43244
|
+
}
|
|
43245
|
+
async function defaultPolicyTextForBootstrap() {
|
|
43246
|
+
return [
|
|
43247
|
+
"version: 1",
|
|
43248
|
+
"tier1_always_approve:",
|
|
43249
|
+
" - state_export",
|
|
43250
|
+
" - state_import",
|
|
43251
|
+
" - state_delete",
|
|
43252
|
+
"approval_channel:",
|
|
43253
|
+
" type: stderr",
|
|
43254
|
+
" timeout_seconds: 300",
|
|
43255
|
+
""
|
|
43256
|
+
].join("\n");
|
|
43257
|
+
}
|
|
42338
43258
|
async function cmdStatus(argv, ctx) {
|
|
42339
43259
|
const tenants = await discoverTenants(ctx.discoverOpts);
|
|
42340
43260
|
const probes = await Promise.all(tenants.map((t) => ctx.probe(t)));
|
|
@@ -42375,6 +43295,7 @@ var init_cli5 = __esm({
|
|
|
42375
43295
|
"src/cli/agents/cli.ts"() {
|
|
42376
43296
|
init_discovery();
|
|
42377
43297
|
init_health();
|
|
43298
|
+
init_loader();
|
|
42378
43299
|
}
|
|
42379
43300
|
});
|
|
42380
43301
|
|
|
@@ -43803,7 +44724,7 @@ async function main() {
|
|
|
43803
44724
|
const code = await runIdentityCommand2({ argv: args.slice(1) });
|
|
43804
44725
|
process.exit(code);
|
|
43805
44726
|
}
|
|
43806
|
-
if (args[0] === "agents") {
|
|
44727
|
+
if (args[0] === "agents" || args[0] === "agent") {
|
|
43807
44728
|
const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
43808
44729
|
const code = await runAgentsCommand2({ argv: args.slice(1) });
|
|
43809
44730
|
process.exit(code);
|