@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.js
CHANGED
|
@@ -4450,9 +4450,35 @@ function validatePolicy(raw) {
|
|
|
4450
4450
|
};
|
|
4451
4451
|
delete merged.auto_deny;
|
|
4452
4452
|
return merged;
|
|
4453
|
-
})()
|
|
4453
|
+
})(),
|
|
4454
|
+
approval_redirect: parseApprovalRedirect(raw.approval_redirect)
|
|
4454
4455
|
};
|
|
4455
4456
|
}
|
|
4457
|
+
function parseApprovalRedirect(raw) {
|
|
4458
|
+
if (raw === void 0 || raw === null) {
|
|
4459
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4460
|
+
}
|
|
4461
|
+
if (typeof raw !== "object") {
|
|
4462
|
+
return { ...DEFAULT_APPROVAL_REDIRECT };
|
|
4463
|
+
}
|
|
4464
|
+
const obj = raw;
|
|
4465
|
+
const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
|
|
4466
|
+
const modeRaw = obj.mode;
|
|
4467
|
+
let mode = DEFAULT_APPROVAL_REDIRECT.mode;
|
|
4468
|
+
if (modeRaw !== void 0) {
|
|
4469
|
+
if (modeRaw !== "replace" && modeRaw !== "notify") {
|
|
4470
|
+
throw new Error(
|
|
4471
|
+
`approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
mode = modeRaw;
|
|
4475
|
+
}
|
|
4476
|
+
const result = { enabled, mode };
|
|
4477
|
+
if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
|
|
4478
|
+
result.per_agent = obj.per_agent;
|
|
4479
|
+
}
|
|
4480
|
+
return result;
|
|
4481
|
+
}
|
|
4456
4482
|
function generateDefaultPolicyYaml() {
|
|
4457
4483
|
return `# Sanctuary Principal Policy v1
|
|
4458
4484
|
# This file controls what your agent can do without asking.
|
|
@@ -4531,6 +4557,7 @@ tier3_always_allow:
|
|
|
4531
4557
|
- handshake_status
|
|
4532
4558
|
- handshake_exchange
|
|
4533
4559
|
- handshake_verify_attestation
|
|
4560
|
+
- handshake_abort
|
|
4534
4561
|
- reputation_query_weighted
|
|
4535
4562
|
- federation_peers
|
|
4536
4563
|
- federation_trust_evaluate
|
|
@@ -4565,6 +4592,21 @@ tier3_always_allow:
|
|
|
4565
4592
|
approval_channel:
|
|
4566
4593
|
type: stderr
|
|
4567
4594
|
timeout_seconds: 300
|
|
4595
|
+
|
|
4596
|
+
# \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
|
|
4597
|
+
# Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
|
|
4598
|
+
# resolve via the unified approval inbox at /api/approval-inbox/* instead
|
|
4599
|
+
# of (or in addition to) the configured approval_channel above.
|
|
4600
|
+
#
|
|
4601
|
+
# mode:
|
|
4602
|
+
# replace: bypass the approval_channel entirely; the gate awaits a
|
|
4603
|
+
# decision from the inbox (default once enabled).
|
|
4604
|
+
# notify: fire BOTH the approval_channel and the inbox; first decision
|
|
4605
|
+
# wins. Right shape for harnesses that cannot fully suppress
|
|
4606
|
+
# their local approval prompt (e.g. Mastra-class).
|
|
4607
|
+
approval_redirect:
|
|
4608
|
+
enabled: false
|
|
4609
|
+
mode: replace
|
|
4568
4610
|
`;
|
|
4569
4611
|
}
|
|
4570
4612
|
async function loadPrincipalPolicy(storagePath) {
|
|
@@ -4601,7 +4643,7 @@ async function loadPrincipalPolicy(storagePath) {
|
|
|
4601
4643
|
);
|
|
4602
4644
|
}
|
|
4603
4645
|
}
|
|
4604
|
-
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4646
|
+
var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_APPROVAL_REDIRECT, DEFAULT_POLICY, MalformedPrincipalPolicyError;
|
|
4605
4647
|
var init_loader = __esm({
|
|
4606
4648
|
"src/principal-policy/loader.ts"() {
|
|
4607
4649
|
DEFAULT_TIER2 = {
|
|
@@ -4618,6 +4660,10 @@ var init_loader = __esm({
|
|
|
4618
4660
|
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
4619
4661
|
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
4620
4662
|
};
|
|
4663
|
+
DEFAULT_APPROVAL_REDIRECT = {
|
|
4664
|
+
enabled: false,
|
|
4665
|
+
mode: "replace"
|
|
4666
|
+
};
|
|
4621
4667
|
DEFAULT_POLICY = {
|
|
4622
4668
|
version: 1,
|
|
4623
4669
|
tier1_always_approve: [
|
|
@@ -4691,6 +4737,7 @@ var init_loader = __esm({
|
|
|
4691
4737
|
"handshake_status",
|
|
4692
4738
|
"handshake_exchange",
|
|
4693
4739
|
"handshake_verify_attestation",
|
|
4740
|
+
"handshake_abort",
|
|
4694
4741
|
"reputation_query_weighted",
|
|
4695
4742
|
"federation_peers",
|
|
4696
4743
|
"federation_trust_evaluate",
|
|
@@ -4732,7 +4779,8 @@ var init_loader = __esm({
|
|
|
4732
4779
|
"compliance_eu_ai_act_annex_iii_classify"
|
|
4733
4780
|
// Read-only; rule-based Annex III classifier
|
|
4734
4781
|
],
|
|
4735
|
-
approval_channel: DEFAULT_CHANNEL
|
|
4782
|
+
approval_channel: DEFAULT_CHANNEL,
|
|
4783
|
+
approval_redirect: DEFAULT_APPROVAL_REDIRECT
|
|
4736
4784
|
};
|
|
4737
4785
|
MalformedPrincipalPolicyError = class extends Error {
|
|
4738
4786
|
constructor(policyPath, reason) {
|
|
@@ -20563,6 +20611,174 @@ var init_approval_aggregator = __esm({
|
|
|
20563
20611
|
}
|
|
20564
20612
|
});
|
|
20565
20613
|
|
|
20614
|
+
// src/principal-policy/channels/aggregator-backed-channel.ts
|
|
20615
|
+
function auditEntryIdFor(request) {
|
|
20616
|
+
return `${request.timestamp}:${request.operation}`;
|
|
20617
|
+
}
|
|
20618
|
+
function statusToDecision(entry) {
|
|
20619
|
+
switch (entry.status) {
|
|
20620
|
+
case "approved":
|
|
20621
|
+
return {
|
|
20622
|
+
decision: "approve",
|
|
20623
|
+
decided_by: "human"
|
|
20624
|
+
};
|
|
20625
|
+
case "denied":
|
|
20626
|
+
return {
|
|
20627
|
+
decision: "deny",
|
|
20628
|
+
decided_by: "human"
|
|
20629
|
+
};
|
|
20630
|
+
case "timeout":
|
|
20631
|
+
case "expired":
|
|
20632
|
+
return {
|
|
20633
|
+
decision: "deny",
|
|
20634
|
+
decided_by: "timeout"
|
|
20635
|
+
};
|
|
20636
|
+
default:
|
|
20637
|
+
return null;
|
|
20638
|
+
}
|
|
20639
|
+
}
|
|
20640
|
+
function makeRedirectResolverFromPolicySupplier(supplier) {
|
|
20641
|
+
return (_request) => {
|
|
20642
|
+
const cfg = supplier().approval_redirect;
|
|
20643
|
+
if (!cfg || cfg.enabled !== true) {
|
|
20644
|
+
return { enabled: false, mode: "replace" };
|
|
20645
|
+
}
|
|
20646
|
+
return {
|
|
20647
|
+
enabled: true,
|
|
20648
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
20649
|
+
};
|
|
20650
|
+
};
|
|
20651
|
+
}
|
|
20652
|
+
var DEFAULT_REPLACE_MODE_TIMEOUT_MS, AggregatorBackedChannel;
|
|
20653
|
+
var init_aggregator_backed_channel = __esm({
|
|
20654
|
+
"src/principal-policy/channels/aggregator-backed-channel.ts"() {
|
|
20655
|
+
DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
20656
|
+
AggregatorBackedChannel = class {
|
|
20657
|
+
underlying;
|
|
20658
|
+
aggregator;
|
|
20659
|
+
resolveRedirect;
|
|
20660
|
+
replaceModeTimeoutMs;
|
|
20661
|
+
now;
|
|
20662
|
+
constructor(opts) {
|
|
20663
|
+
this.underlying = opts.underlying;
|
|
20664
|
+
this.aggregator = opts.aggregator;
|
|
20665
|
+
this.resolveRedirect = opts.resolveRedirect;
|
|
20666
|
+
this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
|
|
20667
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
20668
|
+
}
|
|
20669
|
+
/** Expose underlying for tests / wire-up reuse. */
|
|
20670
|
+
getUnderlying() {
|
|
20671
|
+
return this.underlying;
|
|
20672
|
+
}
|
|
20673
|
+
async requestApproval(request) {
|
|
20674
|
+
const cfg = this.resolveRedirect(request);
|
|
20675
|
+
if (!cfg.enabled) {
|
|
20676
|
+
return this.underlying.requestApproval(request);
|
|
20677
|
+
}
|
|
20678
|
+
if (cfg.mode === "replace") {
|
|
20679
|
+
return this.awaitAggregatorDecision(request);
|
|
20680
|
+
}
|
|
20681
|
+
return this.notifyMode(request);
|
|
20682
|
+
}
|
|
20683
|
+
/**
|
|
20684
|
+
* `replace` mode. Subscribe to the aggregator's event stream BEFORE
|
|
20685
|
+
* checking already-stored entries (avoids a race where the entry resolves
|
|
20686
|
+
* between list and subscribe). Match incoming events to this request by
|
|
20687
|
+
* audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
|
|
20688
|
+
*/
|
|
20689
|
+
async awaitAggregatorDecision(request) {
|
|
20690
|
+
const auditId = auditEntryIdFor(request);
|
|
20691
|
+
return new Promise((resolveOuter) => {
|
|
20692
|
+
let settled = false;
|
|
20693
|
+
let unsubscribe = null;
|
|
20694
|
+
let timeoutHandle = null;
|
|
20695
|
+
const settle = (response) => {
|
|
20696
|
+
if (settled) return;
|
|
20697
|
+
settled = true;
|
|
20698
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
20699
|
+
if (unsubscribe) {
|
|
20700
|
+
try {
|
|
20701
|
+
unsubscribe();
|
|
20702
|
+
} catch {
|
|
20703
|
+
}
|
|
20704
|
+
}
|
|
20705
|
+
resolveOuter(response);
|
|
20706
|
+
};
|
|
20707
|
+
const onEvent = (emit) => {
|
|
20708
|
+
if (emit.type !== "resolved") return;
|
|
20709
|
+
if (emit.entry.audit_log_entry_id !== auditId) return;
|
|
20710
|
+
const mapped = statusToDecision(emit.entry);
|
|
20711
|
+
if (!mapped) return;
|
|
20712
|
+
settle({
|
|
20713
|
+
decision: mapped.decision,
|
|
20714
|
+
decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
|
|
20715
|
+
decided_by: mapped.decided_by
|
|
20716
|
+
});
|
|
20717
|
+
};
|
|
20718
|
+
try {
|
|
20719
|
+
unsubscribe = this.aggregator.onEvent(onEvent);
|
|
20720
|
+
} catch (err) {
|
|
20721
|
+
settle({
|
|
20722
|
+
decision: "deny",
|
|
20723
|
+
decided_at: this.now().toISOString(),
|
|
20724
|
+
decided_by: "channel_failure"
|
|
20725
|
+
});
|
|
20726
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
20727
|
+
}
|
|
20728
|
+
void this.aggregator.list({ limit: 200 }).then((entries) => {
|
|
20729
|
+
for (const entry of entries) {
|
|
20730
|
+
if (entry.audit_log_entry_id !== auditId) continue;
|
|
20731
|
+
const mapped = statusToDecision(entry);
|
|
20732
|
+
if (!mapped) return;
|
|
20733
|
+
settle({
|
|
20734
|
+
decision: mapped.decision,
|
|
20735
|
+
decided_at: entry.resolved_at ?? this.now().toISOString(),
|
|
20736
|
+
decided_by: mapped.decided_by
|
|
20737
|
+
});
|
|
20738
|
+
return;
|
|
20739
|
+
}
|
|
20740
|
+
}).catch(() => {
|
|
20741
|
+
});
|
|
20742
|
+
timeoutHandle = setTimeout(() => {
|
|
20743
|
+
settle({
|
|
20744
|
+
decision: "deny",
|
|
20745
|
+
decided_at: this.now().toISOString(),
|
|
20746
|
+
decided_by: "timeout"
|
|
20747
|
+
});
|
|
20748
|
+
}, this.replaceModeTimeoutMs);
|
|
20749
|
+
});
|
|
20750
|
+
}
|
|
20751
|
+
/**
|
|
20752
|
+
* `notify` mode. Fire the underlying channel and listen on the
|
|
20753
|
+
* aggregator simultaneously; whichever resolves first wins. Both
|
|
20754
|
+
* paths produce identical `ApprovalResponse` shapes; the gate's
|
|
20755
|
+
* downstream audit logging is unchanged.
|
|
20756
|
+
*
|
|
20757
|
+
* On underlying-channel failure, fall through to the aggregator wait
|
|
20758
|
+
* (still bounded by `replaceModeTimeoutMs`). Operator can still
|
|
20759
|
+
* resolve from the inbox even if the dashboard/webhook is down.
|
|
20760
|
+
*/
|
|
20761
|
+
async notifyMode(request) {
|
|
20762
|
+
const aggregatorPromise = this.awaitAggregatorDecision(request);
|
|
20763
|
+
let underlyingPromise;
|
|
20764
|
+
try {
|
|
20765
|
+
underlyingPromise = this.underlying.requestApproval(request);
|
|
20766
|
+
} catch (err) {
|
|
20767
|
+
const response = await aggregatorPromise;
|
|
20768
|
+
return response;
|
|
20769
|
+
}
|
|
20770
|
+
return Promise.race([
|
|
20771
|
+
aggregatorPromise,
|
|
20772
|
+
underlyingPromise.catch(
|
|
20773
|
+
() => new Promise(() => {
|
|
20774
|
+
})
|
|
20775
|
+
)
|
|
20776
|
+
]);
|
|
20777
|
+
}
|
|
20778
|
+
};
|
|
20779
|
+
}
|
|
20780
|
+
});
|
|
20781
|
+
|
|
20566
20782
|
// src/principal-policy/tools.ts
|
|
20567
20783
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
20568
20784
|
return [
|
|
@@ -21466,6 +21682,76 @@ var init_attestation = __esm({
|
|
|
21466
21682
|
}
|
|
21467
21683
|
});
|
|
21468
21684
|
|
|
21685
|
+
// src/handshake/audit.ts
|
|
21686
|
+
function auditHandshakeInitiated(auditLog, ctx) {
|
|
21687
|
+
auditLog.append(
|
|
21688
|
+
"l4",
|
|
21689
|
+
HANDSHAKE_LIFECYCLE_OPS.INITIATED,
|
|
21690
|
+
ctx.identity_id,
|
|
21691
|
+
detailsFromContext(ctx),
|
|
21692
|
+
"success"
|
|
21693
|
+
);
|
|
21694
|
+
}
|
|
21695
|
+
function auditHandshakeCompleted(auditLog, ctx) {
|
|
21696
|
+
const details = detailsFromContext(ctx);
|
|
21697
|
+
if (ctx.trust_tier !== void 0) {
|
|
21698
|
+
details.trust_tier = ctx.trust_tier;
|
|
21699
|
+
}
|
|
21700
|
+
auditLog.append(
|
|
21701
|
+
"l4",
|
|
21702
|
+
HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
|
|
21703
|
+
ctx.identity_id,
|
|
21704
|
+
details,
|
|
21705
|
+
"success"
|
|
21706
|
+
);
|
|
21707
|
+
}
|
|
21708
|
+
function auditHandshakeFailed(auditLog, ctx) {
|
|
21709
|
+
const details = detailsFromContext(ctx);
|
|
21710
|
+
details.reason = ctx.reason;
|
|
21711
|
+
if (ctx.error !== void 0) {
|
|
21712
|
+
details.error = ctx.error;
|
|
21713
|
+
}
|
|
21714
|
+
auditLog.append(
|
|
21715
|
+
"l4",
|
|
21716
|
+
HANDSHAKE_LIFECYCLE_OPS.FAILED,
|
|
21717
|
+
ctx.identity_id,
|
|
21718
|
+
details,
|
|
21719
|
+
"failure"
|
|
21720
|
+
);
|
|
21721
|
+
}
|
|
21722
|
+
function auditHandshakeAborted(auditLog, ctx) {
|
|
21723
|
+
const details = detailsFromContext(ctx);
|
|
21724
|
+
details.reason = ctx.reason;
|
|
21725
|
+
auditLog.append(
|
|
21726
|
+
"l4",
|
|
21727
|
+
HANDSHAKE_LIFECYCLE_OPS.ABORTED,
|
|
21728
|
+
ctx.identity_id,
|
|
21729
|
+
details,
|
|
21730
|
+
"failure"
|
|
21731
|
+
);
|
|
21732
|
+
}
|
|
21733
|
+
function detailsFromContext(ctx) {
|
|
21734
|
+
const details = {
|
|
21735
|
+
session_id: ctx.session_id,
|
|
21736
|
+
role: ctx.role
|
|
21737
|
+
};
|
|
21738
|
+
if (ctx.counterparty_id !== void 0) {
|
|
21739
|
+
details.counterparty_id = ctx.counterparty_id;
|
|
21740
|
+
}
|
|
21741
|
+
return details;
|
|
21742
|
+
}
|
|
21743
|
+
var HANDSHAKE_LIFECYCLE_OPS;
|
|
21744
|
+
var init_audit = __esm({
|
|
21745
|
+
"src/handshake/audit.ts"() {
|
|
21746
|
+
HANDSHAKE_LIFECYCLE_OPS = {
|
|
21747
|
+
INITIATED: "handshake_initiated",
|
|
21748
|
+
COMPLETED: "handshake_completed",
|
|
21749
|
+
FAILED: "handshake_failed",
|
|
21750
|
+
ABORTED: "handshake_aborted"
|
|
21751
|
+
};
|
|
21752
|
+
}
|
|
21753
|
+
});
|
|
21754
|
+
|
|
21469
21755
|
// src/handshake/tools.ts
|
|
21470
21756
|
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
21471
21757
|
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
@@ -21499,6 +21785,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21499
21785
|
const { challenge, session } = initiateHandshake(shr);
|
|
21500
21786
|
sessions.set(session.session_id, session);
|
|
21501
21787
|
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
21788
|
+
auditHandshakeInitiated(auditLog, {
|
|
21789
|
+
session_id: session.session_id,
|
|
21790
|
+
role: "initiator",
|
|
21791
|
+
identity_id: shr.body.instance_id
|
|
21792
|
+
});
|
|
21502
21793
|
return toolResult({
|
|
21503
21794
|
session_id: session.session_id,
|
|
21504
21795
|
challenge,
|
|
@@ -21538,10 +21829,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21538
21829
|
);
|
|
21539
21830
|
if ("error" in result) {
|
|
21540
21831
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
21832
|
+
auditHandshakeFailed(auditLog, {
|
|
21833
|
+
session_id: "unknown",
|
|
21834
|
+
role: "responder",
|
|
21835
|
+
identity_id: shr.body.instance_id,
|
|
21836
|
+
reason: classifyRespondFailure(result.error),
|
|
21837
|
+
error: result.error
|
|
21838
|
+
});
|
|
21541
21839
|
return toolResult({ error: result.error });
|
|
21542
21840
|
}
|
|
21543
21841
|
sessions.set(result.session.session_id, result.session);
|
|
21544
21842
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
21843
|
+
auditHandshakeInitiated(auditLog, {
|
|
21844
|
+
session_id: result.session.session_id,
|
|
21845
|
+
role: "responder",
|
|
21846
|
+
identity_id: shr.body.instance_id,
|
|
21847
|
+
counterparty_id: challenge.shr.body.instance_id
|
|
21848
|
+
});
|
|
21545
21849
|
let autoPublishResult;
|
|
21546
21850
|
if (autoPublishHandshakes) {
|
|
21547
21851
|
autoPublishResult = { attempted: true };
|
|
@@ -21649,9 +21953,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21649
21953
|
const response = args.response;
|
|
21650
21954
|
const session = sessions.get(sessionId);
|
|
21651
21955
|
if (!session) {
|
|
21956
|
+
auditHandshakeFailed(auditLog, {
|
|
21957
|
+
session_id: sessionId,
|
|
21958
|
+
role: "initiator",
|
|
21959
|
+
identity_id: "unknown",
|
|
21960
|
+
reason: "session_unknown",
|
|
21961
|
+
error: `No handshake session found: ${sessionId}`
|
|
21962
|
+
});
|
|
21652
21963
|
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
21653
21964
|
}
|
|
21654
21965
|
if (session.state !== "initiated") {
|
|
21966
|
+
auditHandshakeFailed(auditLog, {
|
|
21967
|
+
session_id: sessionId,
|
|
21968
|
+
role: "initiator",
|
|
21969
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21970
|
+
reason: "session_state_mismatch",
|
|
21971
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21972
|
+
});
|
|
21655
21973
|
return toolResult({
|
|
21656
21974
|
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
21657
21975
|
});
|
|
@@ -21665,6 +21983,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21665
21983
|
if ("error" in result) {
|
|
21666
21984
|
session.state = "failed";
|
|
21667
21985
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
21986
|
+
auditHandshakeFailed(auditLog, {
|
|
21987
|
+
session_id: sessionId,
|
|
21988
|
+
role: "initiator",
|
|
21989
|
+
identity_id: session.our_shr.body.instance_id,
|
|
21990
|
+
reason: classifyCompleteFailure(result.error),
|
|
21991
|
+
error: result.error
|
|
21992
|
+
});
|
|
21668
21993
|
return toolResult({ error: result.error });
|
|
21669
21994
|
}
|
|
21670
21995
|
session.state = "completed";
|
|
@@ -21673,6 +21998,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21673
21998
|
session.result = result.result;
|
|
21674
21999
|
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
21675
22000
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
22001
|
+
auditHandshakeCompleted(auditLog, {
|
|
22002
|
+
session_id: sessionId,
|
|
22003
|
+
role: "initiator",
|
|
22004
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22005
|
+
counterparty_id: result.result.counterparty_id,
|
|
22006
|
+
trust_tier: result.result.trust_tier
|
|
22007
|
+
});
|
|
21676
22008
|
return toolResult({
|
|
21677
22009
|
completion: result.completion,
|
|
21678
22010
|
result: result.result,
|
|
@@ -21720,6 +22052,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21720
22052
|
void 0,
|
|
21721
22053
|
result.verified ? "success" : "failure"
|
|
21722
22054
|
);
|
|
22055
|
+
if (result.verified) {
|
|
22056
|
+
auditHandshakeCompleted(auditLog, {
|
|
22057
|
+
session_id: session.session_id,
|
|
22058
|
+
role: "responder",
|
|
22059
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22060
|
+
counterparty_id: result.counterparty_id,
|
|
22061
|
+
trust_tier: result.trust_tier
|
|
22062
|
+
});
|
|
22063
|
+
} else {
|
|
22064
|
+
auditHandshakeFailed(auditLog, {
|
|
22065
|
+
session_id: session.session_id,
|
|
22066
|
+
role: "responder",
|
|
22067
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22068
|
+
counterparty_id: result.counterparty_id,
|
|
22069
|
+
reason: classifyCompleteFailure(result.errors.join("; ")),
|
|
22070
|
+
error: result.errors.join("; ")
|
|
22071
|
+
});
|
|
22072
|
+
}
|
|
21723
22073
|
return toolResult({ result });
|
|
21724
22074
|
}
|
|
21725
22075
|
return toolResult({
|
|
@@ -21827,10 +22177,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
|
|
|
21827
22177
|
_content_trust: "external"
|
|
21828
22178
|
});
|
|
21829
22179
|
}
|
|
22180
|
+
},
|
|
22181
|
+
{
|
|
22182
|
+
name: "handshake_abort",
|
|
22183
|
+
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.",
|
|
22184
|
+
inputSchema: {
|
|
22185
|
+
type: "object",
|
|
22186
|
+
properties: {
|
|
22187
|
+
session_id: {
|
|
22188
|
+
type: "string",
|
|
22189
|
+
description: "Session ID returned from handshake_initiate / handshake_respond."
|
|
22190
|
+
},
|
|
22191
|
+
reason: {
|
|
22192
|
+
type: "string",
|
|
22193
|
+
enum: [
|
|
22194
|
+
"operator_cancelled",
|
|
22195
|
+
"session_timeout",
|
|
22196
|
+
"transport_dropped",
|
|
22197
|
+
"shutdown",
|
|
22198
|
+
"other"
|
|
22199
|
+
],
|
|
22200
|
+
description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
|
|
22201
|
+
}
|
|
22202
|
+
},
|
|
22203
|
+
required: ["session_id"]
|
|
22204
|
+
},
|
|
22205
|
+
handler: async (args) => {
|
|
22206
|
+
const sessionId = args.session_id;
|
|
22207
|
+
const reason = args.reason ?? "operator_cancelled";
|
|
22208
|
+
const session = sessions.get(sessionId);
|
|
22209
|
+
if (!session) {
|
|
22210
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
22211
|
+
}
|
|
22212
|
+
if (session.state === "completed") {
|
|
22213
|
+
return toolResult({
|
|
22214
|
+
error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
|
|
22215
|
+
});
|
|
22216
|
+
}
|
|
22217
|
+
sessions.delete(sessionId);
|
|
22218
|
+
auditHandshakeAborted(auditLog, {
|
|
22219
|
+
session_id: sessionId,
|
|
22220
|
+
role: session.role,
|
|
22221
|
+
identity_id: session.our_shr.body.instance_id,
|
|
22222
|
+
...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
|
|
22223
|
+
reason
|
|
22224
|
+
});
|
|
22225
|
+
return toolResult({
|
|
22226
|
+
aborted: true,
|
|
22227
|
+
session_id: sessionId,
|
|
22228
|
+
reason
|
|
22229
|
+
});
|
|
22230
|
+
}
|
|
21830
22231
|
}
|
|
21831
22232
|
];
|
|
21832
22233
|
return { tools, handshakeResults };
|
|
21833
22234
|
}
|
|
22235
|
+
function classifyRespondFailure(error) {
|
|
22236
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22237
|
+
if (error.includes("SHR verification failed")) return "shr_invalid";
|
|
22238
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22239
|
+
return "other";
|
|
22240
|
+
}
|
|
22241
|
+
function classifyCompleteFailure(error) {
|
|
22242
|
+
if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
|
|
22243
|
+
if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
|
|
22244
|
+
if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
|
|
22245
|
+
if (error.includes("No identity available")) return "no_signing_identity";
|
|
22246
|
+
return "other";
|
|
22247
|
+
}
|
|
21834
22248
|
var init_tools6 = __esm({
|
|
21835
22249
|
"src/handshake/tools.ts"() {
|
|
21836
22250
|
init_router();
|
|
@@ -21840,6 +22254,7 @@ var init_tools6 = __esm({
|
|
|
21840
22254
|
init_encoding();
|
|
21841
22255
|
init_protocol();
|
|
21842
22256
|
init_attestation();
|
|
22257
|
+
init_audit();
|
|
21843
22258
|
init_verifier();
|
|
21844
22259
|
}
|
|
21845
22260
|
});
|
|
@@ -32975,7 +33390,14 @@ var init_operator_chat_audit_events = __esm({
|
|
|
32975
33390
|
* successful thread removal. Body carries thread_id + turn_count of
|
|
32976
33391
|
* the deleted bundle.
|
|
32977
33392
|
*/
|
|
32978
|
-
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
|
|
33393
|
+
CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
|
|
33394
|
+
/**
|
|
33395
|
+
* Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
|
|
33396
|
+
* the multi-turn coherence fold cannot load the active thread's prior
|
|
33397
|
+
* turns; the concierge degrades to single-turn after emitting. Body
|
|
33398
|
+
* carries thread_id + a stable failure_reason enum.
|
|
33399
|
+
*/
|
|
33400
|
+
CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
|
|
32979
33401
|
};
|
|
32980
33402
|
}
|
|
32981
33403
|
});
|
|
@@ -32988,13 +33410,20 @@ var init_operator_chat_types = __esm({
|
|
|
32988
33410
|
CONCIERGE_THREAD_KEY = "_fortress";
|
|
32989
33411
|
}
|
|
32990
33412
|
});
|
|
33413
|
+
function approxTokenLen(text) {
|
|
33414
|
+
return Math.ceil(text.length / 4);
|
|
33415
|
+
}
|
|
32991
33416
|
function makeEventId(prefix) {
|
|
32992
33417
|
return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
32993
33418
|
}
|
|
33419
|
+
function formatPriorTurnLine(turn) {
|
|
33420
|
+
const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
|
|
33421
|
+
return `${label}: ${turn.content}`;
|
|
33422
|
+
}
|
|
32994
33423
|
function hashOf(input) {
|
|
32995
33424
|
return hashToString(sha256(stringToBytes(input)));
|
|
32996
33425
|
}
|
|
32997
|
-
var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
33426
|
+
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;
|
|
32998
33427
|
var init_operator_chat_service = __esm({
|
|
32999
33428
|
"src/chat/operator-chat-service.ts"() {
|
|
33000
33429
|
init_hashing();
|
|
@@ -33002,6 +33431,10 @@ var init_operator_chat_service = __esm({
|
|
|
33002
33431
|
init_operator_chat_audit_events();
|
|
33003
33432
|
init_operator_chat_types();
|
|
33004
33433
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
33434
|
+
DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
33435
|
+
DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
33436
|
+
DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
33437
|
+
DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
33005
33438
|
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
33006
33439
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
33007
33440
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -33038,6 +33471,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33038
33471
|
piiFilter;
|
|
33039
33472
|
conciergeMaxTokens;
|
|
33040
33473
|
memory;
|
|
33474
|
+
historyWindowTurns;
|
|
33475
|
+
historyFreshnessMs;
|
|
33476
|
+
historyTokenBudget;
|
|
33477
|
+
sessionTtlMs;
|
|
33478
|
+
clock;
|
|
33041
33479
|
/**
|
|
33042
33480
|
* In-memory thread_id assigned to the active concierge session.
|
|
33043
33481
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -33045,6 +33483,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33045
33483
|
* folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
|
|
33046
33484
|
*/
|
|
33047
33485
|
activeMemoryThreadId;
|
|
33486
|
+
/**
|
|
33487
|
+
* Wall-clock ms of the most recent sendConcierge that touched the
|
|
33488
|
+
* active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
|
|
33489
|
+
* check: a fresh sendConcierge after `sessionTtlMs` of quiet
|
|
33490
|
+
* allocates a new thread_id even though the prior one is still
|
|
33491
|
+
* readable from the memory store.
|
|
33492
|
+
*/
|
|
33493
|
+
lastInteractionAt;
|
|
33048
33494
|
constructor(deps) {
|
|
33049
33495
|
this.store = deps.store;
|
|
33050
33496
|
this.auditLog = deps.auditLog;
|
|
@@ -33056,6 +33502,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33056
33502
|
if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
|
|
33057
33503
|
this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
|
|
33058
33504
|
if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
|
|
33505
|
+
this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
|
|
33506
|
+
this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
|
|
33507
|
+
this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
|
|
33508
|
+
this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
|
|
33509
|
+
this.clock = deps.conciergeClock ?? (() => Date.now());
|
|
33059
33510
|
}
|
|
33060
33511
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
33061
33512
|
/**
|
|
@@ -33074,6 +33525,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33074
33525
|
throw new Error("concierge query must not be empty");
|
|
33075
33526
|
}
|
|
33076
33527
|
const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
|
|
33528
|
+
const nowMs = this.clock();
|
|
33529
|
+
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
33530
|
+
this.activeMemoryThreadId = void 0;
|
|
33531
|
+
}
|
|
33077
33532
|
const operatorMessage = {
|
|
33078
33533
|
message_id: randomUUID(),
|
|
33079
33534
|
surface: "concierge",
|
|
@@ -33086,6 +33541,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33086
33541
|
CONCIERGE_THREAD_KEY,
|
|
33087
33542
|
operatorMessage
|
|
33088
33543
|
);
|
|
33544
|
+
let priorTurns = [];
|
|
33545
|
+
let memoryReadFailureReason = null;
|
|
33546
|
+
let activeThreadIdForRound;
|
|
33547
|
+
if (this.memory) {
|
|
33548
|
+
activeThreadIdForRound = this.ensureActiveMemoryThread();
|
|
33549
|
+
const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
|
|
33550
|
+
if (result.ok) {
|
|
33551
|
+
const cutoff = nowMs - this.historyFreshnessMs;
|
|
33552
|
+
const fresh = result.turns.filter((t) => {
|
|
33553
|
+
const ts = Date.parse(t.created_at);
|
|
33554
|
+
return Number.isFinite(ts) && ts >= cutoff;
|
|
33555
|
+
});
|
|
33556
|
+
const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
|
|
33557
|
+
priorTurns = recent;
|
|
33558
|
+
} else {
|
|
33559
|
+
memoryReadFailureReason = result.reason;
|
|
33560
|
+
this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
|
|
33561
|
+
}
|
|
33562
|
+
}
|
|
33089
33563
|
if (this.memory) {
|
|
33090
33564
|
const threadId = this.ensureActiveMemoryThread();
|
|
33091
33565
|
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
@@ -33107,7 +33581,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33107
33581
|
conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
|
|
33108
33582
|
outcome = "substrate_disabled";
|
|
33109
33583
|
} else {
|
|
33110
|
-
const context = await this.assembleConciergeContext();
|
|
33584
|
+
const context = await this.assembleConciergeContext(priorTurns);
|
|
33111
33585
|
const response = await this.substrateSelector.invokeSummarize(
|
|
33112
33586
|
"concierge",
|
|
33113
33587
|
{
|
|
@@ -33146,10 +33620,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33146
33620
|
CONCIERGE_THREAD_KEY,
|
|
33147
33621
|
responseMessage
|
|
33148
33622
|
);
|
|
33623
|
+
let assistantTurnId;
|
|
33149
33624
|
if (this.memory) {
|
|
33150
33625
|
const threadId = this.ensureActiveMemoryThread();
|
|
33151
|
-
await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() =>
|
|
33152
|
-
|
|
33626
|
+
const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
|
|
33627
|
+
if (persisted) assistantTurnId = persisted.turn_id;
|
|
33628
|
+
}
|
|
33629
|
+
if (this.memory && activeThreadIdForRound) {
|
|
33630
|
+
this.lastInteractionAt = nowMs;
|
|
33153
33631
|
}
|
|
33154
33632
|
const payload = {
|
|
33155
33633
|
version: "1.2",
|
|
@@ -33162,7 +33640,12 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33162
33640
|
response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
|
|
33163
33641
|
substrate: servedBy,
|
|
33164
33642
|
latency_ms: latencyMs,
|
|
33165
|
-
outcome
|
|
33643
|
+
outcome,
|
|
33644
|
+
...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
|
|
33645
|
+
...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
|
|
33646
|
+
...this.memory ? {
|
|
33647
|
+
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
33648
|
+
} : {}
|
|
33166
33649
|
};
|
|
33167
33650
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
33168
33651
|
return {
|
|
@@ -33172,6 +33655,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33172
33655
|
outcome
|
|
33173
33656
|
};
|
|
33174
33657
|
}
|
|
33658
|
+
/**
|
|
33659
|
+
* Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
|
|
33660
|
+
* out of `sendConcierge` so the read-fold path stays readable. Emits
|
|
33661
|
+
* with `result: "failure"` since the concierge fell back to
|
|
33662
|
+
* single-turn mode for this round-trip.
|
|
33663
|
+
*/
|
|
33664
|
+
emitMemoryReadFailed(threadId, reason) {
|
|
33665
|
+
const payload = {
|
|
33666
|
+
version: "1.2",
|
|
33667
|
+
event_id: makeEventId("conc-memfail"),
|
|
33668
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33669
|
+
identity_id: this.identityId,
|
|
33670
|
+
kind: "operator_concierge_memory_read_failed",
|
|
33671
|
+
surface: "concierge",
|
|
33672
|
+
thread_id: threadId,
|
|
33673
|
+
failure_reason: reason
|
|
33674
|
+
};
|
|
33675
|
+
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
|
|
33676
|
+
}
|
|
33175
33677
|
/**
|
|
33176
33678
|
* Read the persisted concierge thread, oldest message first. Returns
|
|
33177
33679
|
* an empty array when no thread exists yet.
|
|
@@ -33294,6 +33796,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33294
33796
|
* ## Sanctuary reference
|
|
33295
33797
|
* <static domain reference block>
|
|
33296
33798
|
*
|
|
33799
|
+
* ## Prior conversation ← WP-V1.3-9 Tau-2, when present
|
|
33800
|
+
* OPERATOR: ...
|
|
33801
|
+
* CONCIERGE: ...
|
|
33802
|
+
* ---
|
|
33803
|
+
*
|
|
33297
33804
|
* ## Recent activity
|
|
33298
33805
|
* <recentActivity output>
|
|
33299
33806
|
*
|
|
@@ -33303,37 +33810,69 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
33303
33810
|
* ## Open inbox
|
|
33304
33811
|
* <openInbox output>
|
|
33305
33812
|
* ```
|
|
33306
|
-
|
|
33307
|
-
|
|
33813
|
+
*
|
|
33814
|
+
* The substrate selector ships a `context: string` shape (not a
|
|
33815
|
+
* messages array), so multi-turn coherence is folded as a structured
|
|
33816
|
+
* prior-conversation section with explicit OPERATOR / CONCIERGE
|
|
33817
|
+
* boundaries. Coordinator-CTO guidance: prefer messages-array shape
|
|
33818
|
+
* if available; the v1.2 selector does not expose one, so structured
|
|
33819
|
+
* serialization is the canonical path for v1.3.
|
|
33820
|
+
*/
|
|
33821
|
+
async assembleConciergeContext(priorTurns = []) {
|
|
33308
33822
|
const ref = `## Sanctuary reference
|
|
33309
33823
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
33824
|
+
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
33310
33825
|
if (!this.contextProviders) {
|
|
33311
|
-
return
|
|
33312
|
-
|
|
33313
|
-
|
|
33314
|
-
(no providers wired)
|
|
33315
|
-
|
|
33316
|
-
##
|
|
33317
|
-
(
|
|
33318
|
-
|
|
33319
|
-
## Open inbox
|
|
33320
|
-
(no providers wired)`;
|
|
33826
|
+
return [
|
|
33827
|
+
ref,
|
|
33828
|
+
...priorSection ? [priorSection] : [],
|
|
33829
|
+
"## Recent activity\n(no providers wired)",
|
|
33830
|
+
"## Wrapped agents\n(no providers wired)",
|
|
33831
|
+
"## Open inbox\n(no providers wired)"
|
|
33832
|
+
].join("\n\n");
|
|
33321
33833
|
}
|
|
33322
33834
|
const [activity, agents, inbox] = await Promise.all([
|
|
33323
33835
|
this.contextProviders.recentActivity(),
|
|
33324
33836
|
this.contextProviders.agentInventory(),
|
|
33325
33837
|
this.contextProviders.openInbox()
|
|
33326
33838
|
]);
|
|
33327
|
-
return
|
|
33328
|
-
|
|
33329
|
-
|
|
33330
|
-
|
|
33331
|
-
|
|
33332
|
-
|
|
33333
|
-
${agents}
|
|
33334
|
-
|
|
33335
|
-
|
|
33336
|
-
|
|
33839
|
+
return [
|
|
33840
|
+
ref,
|
|
33841
|
+
...priorSection ? [priorSection] : [],
|
|
33842
|
+
`## Recent activity
|
|
33843
|
+
${activity}`,
|
|
33844
|
+
`## Wrapped agents
|
|
33845
|
+
${agents}`,
|
|
33846
|
+
`## Open inbox
|
|
33847
|
+
${inbox}`
|
|
33848
|
+
].join("\n\n");
|
|
33849
|
+
}
|
|
33850
|
+
/**
|
|
33851
|
+
* Render the prior-conversation section with token-budget enforcement
|
|
33852
|
+
* (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
|
|
33853
|
+
* section exceeds `historyTokenBudget`. Returns an empty string when
|
|
33854
|
+
* the input is empty or when the budget excludes every turn.
|
|
33855
|
+
*/
|
|
33856
|
+
formatPriorTurnsSection(turns) {
|
|
33857
|
+
if (turns.length === 0) return "";
|
|
33858
|
+
const HEADER = "## Prior conversation";
|
|
33859
|
+
const lines = turns.map(formatPriorTurnLine);
|
|
33860
|
+
const headerTokens = approxTokenLen(`${HEADER}
|
|
33861
|
+
`);
|
|
33862
|
+
const sepTokens = approxTokenLen("\n");
|
|
33863
|
+
let runningTokens = headerTokens;
|
|
33864
|
+
let runningLines = [];
|
|
33865
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
33866
|
+
const line = lines[i];
|
|
33867
|
+
const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
33868
|
+
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
33869
|
+
runningTokens += tokens;
|
|
33870
|
+
runningLines.push(line);
|
|
33871
|
+
}
|
|
33872
|
+
if (runningLines.length === 0) return "";
|
|
33873
|
+
runningLines = runningLines.reverse();
|
|
33874
|
+
return `${HEADER}
|
|
33875
|
+
${runningLines.join("\n")}`;
|
|
33337
33876
|
}
|
|
33338
33877
|
// ── audit helpers ────────────────────────────────────────────────────
|
|
33339
33878
|
emit(operation, payload, result) {
|
|
@@ -33538,6 +34077,65 @@ var init_concierge_memory_store = __esm({
|
|
|
33538
34077
|
}
|
|
33539
34078
|
return turns;
|
|
33540
34079
|
}
|
|
34080
|
+
/**
|
|
34081
|
+
* Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
|
|
34082
|
+
* `readThread` collapses every failure mode to an empty array, this
|
|
34083
|
+
* variant returns a discriminated result so the multi-turn fold path
|
|
34084
|
+
* can degrade cleanly + emit `operator_concierge_memory_read_failed`
|
|
34085
|
+
* with a concrete cause.
|
|
34086
|
+
*
|
|
34087
|
+
* - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
|
|
34088
|
+
* - Bundle present, decode + decrypt + schema check pass → ok with turns.
|
|
34089
|
+
* - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
|
|
34090
|
+
* - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
|
|
34091
|
+
* - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
|
|
34092
|
+
* - Storage IO error → `io_failed`.
|
|
34093
|
+
*/
|
|
34094
|
+
async readThreadStrict(threadId, opts) {
|
|
34095
|
+
const key = bundleKey(threadId);
|
|
34096
|
+
let raw;
|
|
34097
|
+
try {
|
|
34098
|
+
raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
|
|
34099
|
+
} catch {
|
|
34100
|
+
return { ok: false, reason: "io_failed" };
|
|
34101
|
+
}
|
|
34102
|
+
if (!raw) return { ok: true, turns: [] };
|
|
34103
|
+
if (raw.length > MAX_BUNDLE_BYTES2) {
|
|
34104
|
+
return { ok: false, reason: "oversize_bundle" };
|
|
34105
|
+
}
|
|
34106
|
+
let envelope;
|
|
34107
|
+
try {
|
|
34108
|
+
envelope = JSON.parse(bytesToString(raw));
|
|
34109
|
+
} catch {
|
|
34110
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34111
|
+
}
|
|
34112
|
+
let plaintext;
|
|
34113
|
+
try {
|
|
34114
|
+
const aad = stringToBytes(threadId);
|
|
34115
|
+
plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
34116
|
+
} catch {
|
|
34117
|
+
return { ok: false, reason: "decrypt_failed" };
|
|
34118
|
+
}
|
|
34119
|
+
let parsed;
|
|
34120
|
+
try {
|
|
34121
|
+
parsed = JSON.parse(bytesToString(plaintext));
|
|
34122
|
+
} catch {
|
|
34123
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34124
|
+
}
|
|
34125
|
+
if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
|
|
34126
|
+
if (parsed.thread_id !== threadId) {
|
|
34127
|
+
return { ok: false, reason: "schema_mismatch" };
|
|
34128
|
+
}
|
|
34129
|
+
let turns = parsed.turns;
|
|
34130
|
+
if (opts?.sinceTurnId !== void 0) {
|
|
34131
|
+
const cutoff = opts.sinceTurnId;
|
|
34132
|
+
turns = turns.filter((t) => t.turn_id > cutoff);
|
|
34133
|
+
}
|
|
34134
|
+
if (opts?.limit !== void 0) {
|
|
34135
|
+
turns = turns.slice(0, opts.limit);
|
|
34136
|
+
}
|
|
34137
|
+
return { ok: true, turns };
|
|
34138
|
+
}
|
|
33541
34139
|
/**
|
|
33542
34140
|
* Enumerate concierge threads in this fortress with summary metadata.
|
|
33543
34141
|
* Sorted newest-first by last_turn_at.
|
|
@@ -36435,7 +37033,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
|
|
|
36435
37033
|
}
|
|
36436
37034
|
return null;
|
|
36437
37035
|
}
|
|
36438
|
-
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
37036
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
|
|
36439
37037
|
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
36440
37038
|
if (!destinationSigner) {
|
|
36441
37039
|
return {
|
|
@@ -36497,8 +37095,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36497
37095
|
}
|
|
36498
37096
|
}
|
|
36499
37097
|
}
|
|
37098
|
+
let plaintext;
|
|
36500
37099
|
try {
|
|
36501
|
-
|
|
37100
|
+
plaintext = decrypt(
|
|
36502
37101
|
item.entry.payload,
|
|
36503
37102
|
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
36504
37103
|
);
|
|
@@ -36507,28 +37106,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36507
37106
|
skipped++;
|
|
36508
37107
|
continue;
|
|
36509
37108
|
}
|
|
36510
|
-
await stateStore.write(
|
|
36511
|
-
item.namespace,
|
|
36512
|
-
item.key,
|
|
36513
|
-
bytesToString(plaintext),
|
|
36514
|
-
destinationSigner.identity_id,
|
|
36515
|
-
destinationSigner.encrypted_private_key,
|
|
36516
|
-
identityEncryptionKey,
|
|
36517
|
-
{
|
|
36518
|
-
content_type: item.entry.metadata.content_type,
|
|
36519
|
-
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
36520
|
-
tags: [
|
|
36521
|
-
...item.entry.metadata.tags ?? [],
|
|
36522
|
-
"exit-import",
|
|
36523
|
-
`source:${item.entry.kid}`
|
|
36524
|
-
]
|
|
36525
|
-
}
|
|
36526
|
-
);
|
|
36527
|
-
imported++;
|
|
36528
37109
|
} catch {
|
|
36529
37110
|
skippedInvalidSig++;
|
|
36530
37111
|
skipped++;
|
|
37112
|
+
continue;
|
|
36531
37113
|
}
|
|
37114
|
+
await stateStore.write(
|
|
37115
|
+
item.namespace,
|
|
37116
|
+
item.key,
|
|
37117
|
+
bytesToString(plaintext),
|
|
37118
|
+
destinationSigner.identity_id,
|
|
37119
|
+
destinationSigner.encrypted_private_key,
|
|
37120
|
+
identityEncryptionKey,
|
|
37121
|
+
{
|
|
37122
|
+
content_type: item.entry.metadata.content_type,
|
|
37123
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
37124
|
+
tags: [
|
|
37125
|
+
...item.entry.metadata.tags ?? [],
|
|
37126
|
+
"exit-import",
|
|
37127
|
+
`source:${item.entry.kid}`
|
|
37128
|
+
]
|
|
37129
|
+
}
|
|
37130
|
+
);
|
|
37131
|
+
imported++;
|
|
37132
|
+
importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
|
|
36532
37133
|
}
|
|
36533
37134
|
return {
|
|
36534
37135
|
status: "rekeyed",
|
|
@@ -36539,6 +37140,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
|
|
|
36539
37140
|
conflicts
|
|
36540
37141
|
};
|
|
36541
37142
|
}
|
|
37143
|
+
async function cleanupStagedPaths(storage, staged) {
|
|
37144
|
+
let removed = 0;
|
|
37145
|
+
const failed = [];
|
|
37146
|
+
for (const loc of staged) {
|
|
37147
|
+
try {
|
|
37148
|
+
const ok2 = await storage.delete(loc.namespace, loc.key);
|
|
37149
|
+
if (ok2) {
|
|
37150
|
+
removed++;
|
|
37151
|
+
} else {
|
|
37152
|
+
failed.push(loc);
|
|
37153
|
+
}
|
|
37154
|
+
} catch {
|
|
37155
|
+
failed.push(loc);
|
|
37156
|
+
}
|
|
37157
|
+
}
|
|
37158
|
+
return { removed, failed };
|
|
37159
|
+
}
|
|
36542
37160
|
async function stageArtifact(storage, namespace, key, value) {
|
|
36543
37161
|
await storage.write(namespace, key, jsonBytes(value));
|
|
36544
37162
|
}
|
|
@@ -36663,6 +37281,8 @@ async function importExitBundle(opts) {
|
|
|
36663
37281
|
}
|
|
36664
37282
|
const importId = importIdForManifest(manifest);
|
|
36665
37283
|
const stagedArtifacts = [];
|
|
37284
|
+
const stagedLocations = [];
|
|
37285
|
+
const importedRekeyEntries = [];
|
|
36666
37286
|
if (identityArtifact) {
|
|
36667
37287
|
await stageArtifact(
|
|
36668
37288
|
opts.storage,
|
|
@@ -36671,10 +37291,15 @@ async function importExitBundle(opts) {
|
|
|
36671
37291
|
identityArtifact.json
|
|
36672
37292
|
);
|
|
36673
37293
|
stagedArtifacts.push("public_identity");
|
|
37294
|
+
stagedLocations.push({
|
|
37295
|
+
namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
37296
|
+
key: identityArtifact.json.bundle.identity_id
|
|
37297
|
+
});
|
|
36674
37298
|
}
|
|
36675
37299
|
if (policySet) {
|
|
36676
37300
|
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
36677
37301
|
stagedArtifacts.push("policy_set");
|
|
37302
|
+
stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
|
|
36678
37303
|
}
|
|
36679
37304
|
if (auditReceipts) {
|
|
36680
37305
|
await stageArtifact(
|
|
@@ -36684,10 +37309,12 @@ async function importExitBundle(opts) {
|
|
|
36684
37309
|
auditReceipts.json
|
|
36685
37310
|
);
|
|
36686
37311
|
stagedArtifacts.push("audit_receipts");
|
|
37312
|
+
stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
|
|
36687
37313
|
}
|
|
36688
37314
|
if (commitments) {
|
|
36689
37315
|
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
36690
37316
|
stagedArtifacts.push("commitments");
|
|
37317
|
+
stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
|
|
36691
37318
|
}
|
|
36692
37319
|
if (placeholderMetadata) {
|
|
36693
37320
|
await stageArtifact(
|
|
@@ -36697,12 +37324,17 @@ async function importExitBundle(opts) {
|
|
|
36697
37324
|
placeholderMetadata.json
|
|
36698
37325
|
);
|
|
36699
37326
|
stagedArtifacts.push("placeholder_vault_metadata");
|
|
37327
|
+
stagedLocations.push({
|
|
37328
|
+
namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
37329
|
+
key: importId
|
|
37330
|
+
});
|
|
36700
37331
|
}
|
|
36701
37332
|
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
36702
37333
|
manifest: manifest.body,
|
|
36703
37334
|
verified_at: verification.verified_at,
|
|
36704
37335
|
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
36705
37336
|
});
|
|
37337
|
+
stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
|
|
36706
37338
|
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
36707
37339
|
let reputationResult = {
|
|
36708
37340
|
imported_attestations: 0,
|
|
@@ -36727,26 +37359,57 @@ async function importExitBundle(opts) {
|
|
|
36727
37359
|
encryptedState?.json ?? null,
|
|
36728
37360
|
opts
|
|
36729
37361
|
);
|
|
36730
|
-
|
|
36731
|
-
|
|
36732
|
-
|
|
36733
|
-
|
|
36734
|
-
|
|
36735
|
-
|
|
36736
|
-
|
|
36737
|
-
|
|
36738
|
-
|
|
36739
|
-
|
|
36740
|
-
|
|
36741
|
-
|
|
36742
|
-
|
|
36743
|
-
|
|
36744
|
-
|
|
36745
|
-
|
|
36746
|
-
|
|
36747
|
-
|
|
36748
|
-
|
|
36749
|
-
|
|
37362
|
+
let stateResult;
|
|
37363
|
+
try {
|
|
37364
|
+
stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
37365
|
+
encryptedState.json,
|
|
37366
|
+
opts,
|
|
37367
|
+
sourceMasterKey,
|
|
37368
|
+
publicKeys.byIdentityId,
|
|
37369
|
+
importedRekeyEntries
|
|
37370
|
+
) : {
|
|
37371
|
+
status: "staged_requires_source_key",
|
|
37372
|
+
imported_keys: 0,
|
|
37373
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
37374
|
+
skipped_invalid_sig: 0,
|
|
37375
|
+
skipped_unknown_kid: 0,
|
|
37376
|
+
conflicts: conflicts.state_conflicts.length
|
|
37377
|
+
} : {
|
|
37378
|
+
status: "not_requested",
|
|
37379
|
+
imported_keys: 0,
|
|
37380
|
+
skipped_keys: 0,
|
|
37381
|
+
skipped_invalid_sig: 0,
|
|
37382
|
+
skipped_unknown_kid: 0,
|
|
37383
|
+
conflicts: 0
|
|
37384
|
+
};
|
|
37385
|
+
} catch (err) {
|
|
37386
|
+
const toCleanup = [
|
|
37387
|
+
...importedRekeyEntries,
|
|
37388
|
+
...stagedLocations
|
|
37389
|
+
];
|
|
37390
|
+
const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
|
|
37391
|
+
opts.auditLog.append(
|
|
37392
|
+
"l1",
|
|
37393
|
+
"exit_bundle_rekey_failed_cleanup",
|
|
37394
|
+
manifest.body.identity_binding.identity_id,
|
|
37395
|
+
{
|
|
37396
|
+
import_id: importId,
|
|
37397
|
+
manifest_version: manifest.body.manifest_version,
|
|
37398
|
+
rekey_entries_removed: importedRekeyEntries.length,
|
|
37399
|
+
staged_artifacts_removed: stagedLocations.length,
|
|
37400
|
+
removed_total: cleanup.removed,
|
|
37401
|
+
cleanup_failed_count: cleanup.failed.length,
|
|
37402
|
+
original_error: err instanceof Error ? err.message : String(err)
|
|
37403
|
+
},
|
|
37404
|
+
"failure"
|
|
37405
|
+
);
|
|
37406
|
+
await opts.auditLog.flush();
|
|
37407
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
37408
|
+
throw new ExitBundleImportError(
|
|
37409
|
+
"REKEY_FAILED_AND_CLEANED",
|
|
37410
|
+
`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).`
|
|
37411
|
+
);
|
|
37412
|
+
}
|
|
36750
37413
|
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
36751
37414
|
import_id: importId,
|
|
36752
37415
|
manifest_version: manifest.body.manifest_version,
|
|
@@ -37825,7 +38488,6 @@ ${err.message}
|
|
|
37825
38488
|
timestamp: alert.timestamp
|
|
37826
38489
|
});
|
|
37827
38490
|
} : void 0;
|
|
37828
|
-
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
37829
38491
|
const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
|
|
37830
38492
|
const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
|
|
37831
38493
|
const approvalAggregator = new ApprovalAggregator({
|
|
@@ -37835,6 +38497,20 @@ ${err.message}
|
|
|
37835
38497
|
identityId: aggregatorIdentityId,
|
|
37836
38498
|
fortressId: fortressIdForAggregator
|
|
37837
38499
|
});
|
|
38500
|
+
const wrappedApprovalChannel = new AggregatorBackedChannel({
|
|
38501
|
+
underlying: approvalChannel,
|
|
38502
|
+
aggregator: approvalAggregator,
|
|
38503
|
+
resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
|
|
38504
|
+
replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
|
|
38505
|
+
});
|
|
38506
|
+
const gate = new ApprovalGate(
|
|
38507
|
+
policy,
|
|
38508
|
+
baseline,
|
|
38509
|
+
wrappedApprovalChannel,
|
|
38510
|
+
auditLog,
|
|
38511
|
+
injectionDetector,
|
|
38512
|
+
onInjectionAlert
|
|
38513
|
+
);
|
|
37838
38514
|
gate.setApprovalEventCallback((event) => {
|
|
37839
38515
|
void approvalAggregator.ingest(event);
|
|
37840
38516
|
});
|
|
@@ -38032,6 +38708,7 @@ var init_src = __esm({
|
|
|
38032
38708
|
init_webhook();
|
|
38033
38709
|
init_gate();
|
|
38034
38710
|
init_approval_aggregator();
|
|
38711
|
+
init_aggregator_backed_channel();
|
|
38035
38712
|
init_tools4();
|
|
38036
38713
|
init_router();
|
|
38037
38714
|
init_router();
|
|
@@ -40996,6 +41673,22 @@ var init_broker = __esm({
|
|
|
40996
41673
|
auditLog;
|
|
40997
41674
|
issuer;
|
|
40998
41675
|
principalIdentityId;
|
|
41676
|
+
/**
|
|
41677
|
+
* Per-secret-name mutex. Hardening wave 6 finding #64: two concurrent
|
|
41678
|
+
* addSecret() / rotateSecret() / deleteSecret() calls on the same name
|
|
41679
|
+
* MUST serialize cleanly. The keychain backend's `find-then-add` and
|
|
41680
|
+
* `find-then-delete-then-add` shapes (KeychainBackend.addSecret /
|
|
41681
|
+
* .rotateSecret) are not atomic against another caller racing the same
|
|
41682
|
+
* service-name; without serialization the second caller can observe a
|
|
41683
|
+
* stale "exists" check and either drop the new value or leave a
|
|
41684
|
+
* duplicate keychain entry.
|
|
41685
|
+
*
|
|
41686
|
+
* Implementation: an in-memory promise chain per name. Subsequent
|
|
41687
|
+
* callers `await` the chain tail and append their own work; failures
|
|
41688
|
+
* propagate to the failing caller without poisoning the chain for
|
|
41689
|
+
* later callers.
|
|
41690
|
+
*/
|
|
41691
|
+
nameLocks = /* @__PURE__ */ new Map();
|
|
40999
41692
|
constructor(opts) {
|
|
41000
41693
|
this.backend = opts.backend;
|
|
41001
41694
|
this.auditLog = opts.auditLog;
|
|
@@ -41006,6 +41699,40 @@ var init_broker = __esm({
|
|
|
41006
41699
|
grants: opts.grants
|
|
41007
41700
|
});
|
|
41008
41701
|
}
|
|
41702
|
+
/**
|
|
41703
|
+
* Serialize `op` against any other in-flight write to the same secret
|
|
41704
|
+
* `name`. Per-name fairness only, distinct names run in parallel.
|
|
41705
|
+
* The current chain tail is used as the acceptance gate; we then
|
|
41706
|
+
* publish a new tail that swallows the operation's outcome so a
|
|
41707
|
+
* thrown error does not poison the next caller's wait.
|
|
41708
|
+
*/
|
|
41709
|
+
async withNameLock(name, op) {
|
|
41710
|
+
const previous = this.nameLocks.get(name) ?? Promise.resolve();
|
|
41711
|
+
let release = () => {
|
|
41712
|
+
};
|
|
41713
|
+
const next = new Promise((resolve8) => {
|
|
41714
|
+
release = resolve8;
|
|
41715
|
+
});
|
|
41716
|
+
this.nameLocks.set(name, next);
|
|
41717
|
+
try {
|
|
41718
|
+
await previous.catch(() => {
|
|
41719
|
+
});
|
|
41720
|
+
return await op();
|
|
41721
|
+
} finally {
|
|
41722
|
+
release();
|
|
41723
|
+
if (this.nameLocks.get(name) === next) {
|
|
41724
|
+
this.nameLocks.delete(name);
|
|
41725
|
+
}
|
|
41726
|
+
}
|
|
41727
|
+
}
|
|
41728
|
+
/**
|
|
41729
|
+
* Diagnostic-only: visible for tests so they can assert that distinct
|
|
41730
|
+
* names do not contend on a shared lock. Not part of the public broker
|
|
41731
|
+
* contract; do not consume from production code.
|
|
41732
|
+
*/
|
|
41733
|
+
__nameLockCountForTests() {
|
|
41734
|
+
return this.nameLocks.size;
|
|
41735
|
+
}
|
|
41009
41736
|
/** Ensure backend is initialized and unlocked. Audits the unlock. */
|
|
41010
41737
|
async ensureUnlocked(passphrase) {
|
|
41011
41738
|
await this.backend.ensureInitialized(passphrase);
|
|
@@ -41018,31 +41745,37 @@ var init_broker = __esm({
|
|
|
41018
41745
|
);
|
|
41019
41746
|
}
|
|
41020
41747
|
async addSecret(name, value) {
|
|
41021
|
-
await this.
|
|
41022
|
-
|
|
41023
|
-
|
|
41024
|
-
|
|
41025
|
-
|
|
41026
|
-
|
|
41027
|
-
|
|
41748
|
+
await this.withNameLock(name, async () => {
|
|
41749
|
+
await this.backend.addSecret(name, value);
|
|
41750
|
+
this.auditLog.append(
|
|
41751
|
+
"l3",
|
|
41752
|
+
BROKER_OPS.SECRET_ADDED,
|
|
41753
|
+
this.principalIdentityId,
|
|
41754
|
+
{ secret: name }
|
|
41755
|
+
);
|
|
41756
|
+
});
|
|
41028
41757
|
}
|
|
41029
41758
|
async rotateSecret(name, newValue) {
|
|
41030
|
-
await this.
|
|
41031
|
-
|
|
41032
|
-
|
|
41033
|
-
|
|
41034
|
-
|
|
41035
|
-
|
|
41036
|
-
|
|
41759
|
+
await this.withNameLock(name, async () => {
|
|
41760
|
+
await this.backend.rotateSecret(name, newValue);
|
|
41761
|
+
this.auditLog.append(
|
|
41762
|
+
"l3",
|
|
41763
|
+
BROKER_OPS.SECRET_ROTATED,
|
|
41764
|
+
this.principalIdentityId,
|
|
41765
|
+
{ secret: name }
|
|
41766
|
+
);
|
|
41767
|
+
});
|
|
41037
41768
|
}
|
|
41038
41769
|
async deleteSecret(name) {
|
|
41039
|
-
await this.
|
|
41040
|
-
|
|
41041
|
-
|
|
41042
|
-
|
|
41043
|
-
|
|
41044
|
-
|
|
41045
|
-
|
|
41770
|
+
await this.withNameLock(name, async () => {
|
|
41771
|
+
await this.backend.deleteSecret(name);
|
|
41772
|
+
this.auditLog.append(
|
|
41773
|
+
"l3",
|
|
41774
|
+
BROKER_OPS.SECRET_DELETED,
|
|
41775
|
+
this.principalIdentityId,
|
|
41776
|
+
{ secret: name }
|
|
41777
|
+
);
|
|
41778
|
+
});
|
|
41046
41779
|
}
|
|
41047
41780
|
async listSecretNames() {
|
|
41048
41781
|
return this.backend.listSecretNames();
|
|
@@ -41082,6 +41815,19 @@ var init_broker = __esm({
|
|
|
41082
41815
|
liveTokenCount() {
|
|
41083
41816
|
return this.issuer.liveTokenCount();
|
|
41084
41817
|
}
|
|
41818
|
+
/**
|
|
41819
|
+
* Drop expired tokens from the in-memory issuer map. Hardening wave 6
|
|
41820
|
+
* finding #86: previously expiry pruning depended on opportunistic
|
|
41821
|
+
* `pruneExpired()` calls; now the cocoon-unlock initialization path
|
|
41822
|
+
* (openBroker -> after backend.ensureInitialized -> after Broker
|
|
41823
|
+
* construction) fires this once so each cocoon-unlock cycle drops
|
|
41824
|
+
* stale bindings before any operator interaction.
|
|
41825
|
+
*
|
|
41826
|
+
* Returns the number of tokens removed. Safe to call repeatedly; idempotent.
|
|
41827
|
+
*/
|
|
41828
|
+
pruneExpiredTokens() {
|
|
41829
|
+
return this.issuer.pruneExpired();
|
|
41830
|
+
}
|
|
41085
41831
|
/**
|
|
41086
41832
|
* Audit query restricted to broker-scoped operations. Returns entries
|
|
41087
41833
|
* with their timestamps, op, and result (never the secret value).
|
|
@@ -41240,6 +41986,7 @@ async function openBroker(opts = {}) {
|
|
|
41240
41986
|
grants,
|
|
41241
41987
|
principalIdentityId: opts.principalIdentityId ?? "sanctuary-broker"
|
|
41242
41988
|
});
|
|
41989
|
+
broker.pruneExpiredTokens();
|
|
41243
41990
|
return {
|
|
41244
41991
|
broker,
|
|
41245
41992
|
close: async () => {
|
|
@@ -42108,8 +42855,6 @@ var init_health = __esm({
|
|
|
42108
42855
|
DEFAULT_TIMEOUT_MS4 = 500;
|
|
42109
42856
|
}
|
|
42110
42857
|
});
|
|
42111
|
-
|
|
42112
|
-
// src/cli/agents/cli.ts
|
|
42113
42858
|
function resolveCtx(args) {
|
|
42114
42859
|
const env = args.env ?? process.env;
|
|
42115
42860
|
const discoverOpts = {
|
|
@@ -42147,6 +42892,8 @@ async function runAgentsCommand(args) {
|
|
|
42147
42892
|
return await cmdShow2(rest, ctx);
|
|
42148
42893
|
case "status":
|
|
42149
42894
|
return await cmdStatus(rest, ctx);
|
|
42895
|
+
case "config":
|
|
42896
|
+
return await cmdConfig(rest, ctx);
|
|
42150
42897
|
default:
|
|
42151
42898
|
ctx.err.write(`Unknown subcommand: ${sub}
|
|
42152
42899
|
`);
|
|
@@ -42162,10 +42909,18 @@ async function runAgentsCommand(args) {
|
|
|
42162
42909
|
}
|
|
42163
42910
|
function printUsage4(s) {
|
|
42164
42911
|
s.write(`Usage: sanctuary agents <command> [flags]
|
|
42912
|
+
sanctuary agent <command> [flags] (alias)
|
|
42165
42913
|
|
|
42166
42914
|
list [--json] List every tenant visible on this host.
|
|
42167
|
-
show <tenant> [--json] Show details for one tenant
|
|
42915
|
+
show <tenant> [--json] Show details for one tenant (includes
|
|
42916
|
+
approval-redirect state).
|
|
42168
42917
|
status [--json] One-line-per-tenant running/stopped summary.
|
|
42918
|
+
config <tenant> [opts] Write tenant principal-policy.yaml fields.
|
|
42919
|
+
--approval-redirect=<bool> Toggle cross-harness inbox redirect.
|
|
42920
|
+
--approval-redirect-mode=<replace|notify>
|
|
42921
|
+
Pick replace (bypass underlying channel)
|
|
42922
|
+
or notify (race both paths). Default
|
|
42923
|
+
replace when toggled on.
|
|
42169
42924
|
|
|
42170
42925
|
Options:
|
|
42171
42926
|
--fortress <path> Scope discovery to a specific storage path
|
|
@@ -42267,6 +43022,7 @@ async function cmdShow2(argv, ctx) {
|
|
|
42267
43022
|
return 1;
|
|
42268
43023
|
}
|
|
42269
43024
|
const probe = await ctx.probe(tenant);
|
|
43025
|
+
const approvalRedirect = await readApprovalRedirectState(tenant);
|
|
42270
43026
|
const payload = {
|
|
42271
43027
|
name: tenant.name,
|
|
42272
43028
|
storage_path: tenant.storage_path,
|
|
@@ -42280,7 +43036,8 @@ async function cmdShow2(argv, ctx) {
|
|
|
42280
43036
|
running: probe.running,
|
|
42281
43037
|
status: probe.status,
|
|
42282
43038
|
reason: probe.reason
|
|
42283
|
-
}
|
|
43039
|
+
},
|
|
43040
|
+
approval_redirect: approvalRedirect
|
|
42284
43041
|
};
|
|
42285
43042
|
if (hasJsonFlag(argv)) {
|
|
42286
43043
|
ctx.out.write(JSON.stringify(payload, null, 2) + "\n");
|
|
@@ -42324,10 +43081,173 @@ async function cmdShow2(argv, ctx) {
|
|
|
42324
43081
|
}
|
|
42325
43082
|
ctx.out.write(
|
|
42326
43083
|
`probe: ${probe.running ? "running" : "not-running"}${probe.reason ? ` (${probe.reason})` : ""}
|
|
43084
|
+
`
|
|
43085
|
+
);
|
|
43086
|
+
ctx.out.write(
|
|
43087
|
+
`approval_redirect: ${approvalRedirect.enabled ? `on (${approvalRedirect.mode})` : "off"}
|
|
42327
43088
|
`
|
|
42328
43089
|
);
|
|
42329
43090
|
return 0;
|
|
42330
43091
|
}
|
|
43092
|
+
async function readApprovalRedirectState(tenant) {
|
|
43093
|
+
const policyPath = join(tenant.storage_path, "principal-policy.yaml");
|
|
43094
|
+
try {
|
|
43095
|
+
const content = await readFile(policyPath, "utf-8");
|
|
43096
|
+
const parsed = parsePolicy(content);
|
|
43097
|
+
const cfg = parsed.approval_redirect;
|
|
43098
|
+
if (!cfg) return { enabled: false, mode: "replace" };
|
|
43099
|
+
return {
|
|
43100
|
+
enabled: !!cfg.enabled,
|
|
43101
|
+
mode: cfg.mode === "notify" ? "notify" : "replace"
|
|
43102
|
+
};
|
|
43103
|
+
} catch {
|
|
43104
|
+
return { enabled: false, mode: "replace" };
|
|
43105
|
+
}
|
|
43106
|
+
}
|
|
43107
|
+
function parseBoolFlag(raw) {
|
|
43108
|
+
if (raw === void 0) return null;
|
|
43109
|
+
const v = raw.toLowerCase();
|
|
43110
|
+
if (v === "true" || v === "yes" || v === "on" || v === "1") return true;
|
|
43111
|
+
if (v === "false" || v === "no" || v === "off" || v === "0") return false;
|
|
43112
|
+
return null;
|
|
43113
|
+
}
|
|
43114
|
+
function findFlagValue(argv, name) {
|
|
43115
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43116
|
+
const a = argv[i];
|
|
43117
|
+
if (a === name) {
|
|
43118
|
+
return argv[i + 1];
|
|
43119
|
+
}
|
|
43120
|
+
const eq = `${name}=`;
|
|
43121
|
+
if (a.startsWith(eq)) {
|
|
43122
|
+
return a.slice(eq.length);
|
|
43123
|
+
}
|
|
43124
|
+
}
|
|
43125
|
+
return void 0;
|
|
43126
|
+
}
|
|
43127
|
+
async function cmdConfig(argv, ctx) {
|
|
43128
|
+
const positional = argv.find((a) => !a.startsWith("--"));
|
|
43129
|
+
if (!positional) {
|
|
43130
|
+
ctx.err.write(
|
|
43131
|
+
"Missing tenant. Usage: sanctuary agents config <tenant> --approval-redirect=<bool>\n"
|
|
43132
|
+
);
|
|
43133
|
+
return 2;
|
|
43134
|
+
}
|
|
43135
|
+
const tenant = await findTenant(positional, ctx.discoverOpts);
|
|
43136
|
+
if (!tenant) {
|
|
43137
|
+
ctx.err.write(`sanctuary agents: unknown tenant "${positional}"
|
|
43138
|
+
`);
|
|
43139
|
+
return 1;
|
|
43140
|
+
}
|
|
43141
|
+
const redirectFlag = parseBoolFlag(
|
|
43142
|
+
findFlagValue(argv, "--approval-redirect")
|
|
43143
|
+
);
|
|
43144
|
+
const modeFlag = findFlagValue(argv, "--approval-redirect-mode");
|
|
43145
|
+
if (redirectFlag === null && modeFlag === void 0) {
|
|
43146
|
+
ctx.err.write(
|
|
43147
|
+
"sanctuary agents config: nothing to do. Pass --approval-redirect=<bool> or --approval-redirect-mode=<replace|notify>.\n"
|
|
43148
|
+
);
|
|
43149
|
+
return 2;
|
|
43150
|
+
}
|
|
43151
|
+
if (modeFlag !== void 0 && modeFlag !== "replace" && modeFlag !== "notify") {
|
|
43152
|
+
ctx.err.write(
|
|
43153
|
+
`sanctuary agents config: --approval-redirect-mode must be "replace" or "notify" (got "${modeFlag}")
|
|
43154
|
+
`
|
|
43155
|
+
);
|
|
43156
|
+
return 2;
|
|
43157
|
+
}
|
|
43158
|
+
const current = await readApprovalRedirectState(tenant);
|
|
43159
|
+
const next = {
|
|
43160
|
+
enabled: redirectFlag !== null ? redirectFlag : current.enabled,
|
|
43161
|
+
mode: modeFlag === "notify" || modeFlag === "replace" ? modeFlag : current.mode
|
|
43162
|
+
};
|
|
43163
|
+
await writeApprovalRedirectToPolicyFile(tenant.storage_path, next);
|
|
43164
|
+
if (hasJsonFlag(argv)) {
|
|
43165
|
+
ctx.out.write(
|
|
43166
|
+
JSON.stringify(
|
|
43167
|
+
{
|
|
43168
|
+
tenant: tenant.name,
|
|
43169
|
+
approval_redirect: next
|
|
43170
|
+
},
|
|
43171
|
+
null,
|
|
43172
|
+
2
|
|
43173
|
+
) + "\n"
|
|
43174
|
+
);
|
|
43175
|
+
} else {
|
|
43176
|
+
ctx.out.write(
|
|
43177
|
+
`sanctuary agents config: tenant "${tenant.name}" approval_redirect=${next.enabled ? `on (${next.mode})` : "off"}
|
|
43178
|
+
`
|
|
43179
|
+
);
|
|
43180
|
+
ctx.out.write(
|
|
43181
|
+
` Takes effect on the next gate request for the running server.
|
|
43182
|
+
`
|
|
43183
|
+
);
|
|
43184
|
+
}
|
|
43185
|
+
return 0;
|
|
43186
|
+
}
|
|
43187
|
+
async function writeApprovalRedirectToPolicyFile(storagePath, state) {
|
|
43188
|
+
const policyPath = join(storagePath, "principal-policy.yaml");
|
|
43189
|
+
let content;
|
|
43190
|
+
try {
|
|
43191
|
+
content = await readFile(policyPath, "utf-8");
|
|
43192
|
+
} catch (err) {
|
|
43193
|
+
const code = err?.code;
|
|
43194
|
+
if (code !== "ENOENT") throw err;
|
|
43195
|
+
content = await defaultPolicyTextForBootstrap();
|
|
43196
|
+
}
|
|
43197
|
+
const block = renderApprovalRedirectBlock(state);
|
|
43198
|
+
const updated = upsertApprovalRedirectBlock(content, block);
|
|
43199
|
+
await writeFile(policyPath, updated, "utf-8");
|
|
43200
|
+
await chmod(policyPath, 384);
|
|
43201
|
+
}
|
|
43202
|
+
function renderApprovalRedirectBlock(state) {
|
|
43203
|
+
return [
|
|
43204
|
+
"# Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2)",
|
|
43205
|
+
"approval_redirect:",
|
|
43206
|
+
` enabled: ${state.enabled ? "true" : "false"}`,
|
|
43207
|
+
` mode: ${state.mode}`
|
|
43208
|
+
].join("\n");
|
|
43209
|
+
}
|
|
43210
|
+
function upsertApprovalRedirectBlock(content, block) {
|
|
43211
|
+
const lines = content.split("\n");
|
|
43212
|
+
const startIdx = lines.findIndex((l) => l.startsWith("approval_redirect:"));
|
|
43213
|
+
if (startIdx === -1) {
|
|
43214
|
+
const trimmed = content.endsWith("\n") ? content : content + "\n";
|
|
43215
|
+
return trimmed + "\n" + block + "\n";
|
|
43216
|
+
}
|
|
43217
|
+
let blockStart = startIdx;
|
|
43218
|
+
if (blockStart > 0 && lines[blockStart - 1] !== void 0 && lines[blockStart - 1].startsWith("# Approval Redirect")) {
|
|
43219
|
+
blockStart = blockStart - 1;
|
|
43220
|
+
}
|
|
43221
|
+
let blockEnd = startIdx + 1;
|
|
43222
|
+
while (blockEnd < lines.length) {
|
|
43223
|
+
const l = lines[blockEnd];
|
|
43224
|
+
if (l === "") {
|
|
43225
|
+
blockEnd++;
|
|
43226
|
+
continue;
|
|
43227
|
+
}
|
|
43228
|
+
if (/^[A-Za-z0-9#]/.test(l)) {
|
|
43229
|
+
break;
|
|
43230
|
+
}
|
|
43231
|
+
blockEnd++;
|
|
43232
|
+
}
|
|
43233
|
+
const before = lines.slice(0, blockStart);
|
|
43234
|
+
const after = lines.slice(blockEnd);
|
|
43235
|
+
const replaced = [...before, ...block.split("\n"), ...after].join("\n");
|
|
43236
|
+
return replaced.endsWith("\n") ? replaced : replaced + "\n";
|
|
43237
|
+
}
|
|
43238
|
+
async function defaultPolicyTextForBootstrap() {
|
|
43239
|
+
return [
|
|
43240
|
+
"version: 1",
|
|
43241
|
+
"tier1_always_approve:",
|
|
43242
|
+
" - state_export",
|
|
43243
|
+
" - state_import",
|
|
43244
|
+
" - state_delete",
|
|
43245
|
+
"approval_channel:",
|
|
43246
|
+
" type: stderr",
|
|
43247
|
+
" timeout_seconds: 300",
|
|
43248
|
+
""
|
|
43249
|
+
].join("\n");
|
|
43250
|
+
}
|
|
42331
43251
|
async function cmdStatus(argv, ctx) {
|
|
42332
43252
|
const tenants = await discoverTenants(ctx.discoverOpts);
|
|
42333
43253
|
const probes = await Promise.all(tenants.map((t) => ctx.probe(t)));
|
|
@@ -42368,6 +43288,7 @@ var init_cli5 = __esm({
|
|
|
42368
43288
|
"src/cli/agents/cli.ts"() {
|
|
42369
43289
|
init_discovery();
|
|
42370
43290
|
init_health();
|
|
43291
|
+
init_loader();
|
|
42371
43292
|
}
|
|
42372
43293
|
});
|
|
42373
43294
|
|
|
@@ -43796,7 +44717,7 @@ async function main() {
|
|
|
43796
44717
|
const code = await runIdentityCommand2({ argv: args.slice(1) });
|
|
43797
44718
|
process.exit(code);
|
|
43798
44719
|
}
|
|
43799
|
-
if (args[0] === "agents") {
|
|
44720
|
+
if (args[0] === "agents" || args[0] === "agent") {
|
|
43800
44721
|
const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
|
|
43801
44722
|
const code = await runAgentsCommand2({ argv: args.slice(1) });
|
|
43802
44723
|
process.exit(code);
|