@wrongstack/core 0.298.0 → 0.298.2
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/coordination/index.js +92 -7
- package/dist/coordination/mail-tools.d.ts +3 -3
- package/dist/coordination/mailbox-codecs.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +32 -1
- package/dist/coordination/mailbox-types.d.ts +158 -3
- package/dist/coordination/model-matrix.d.ts +16 -0
- package/dist/core/index.js +447 -43
- package/dist/defaults/index.js +86 -63
- package/dist/index.js +816 -674
- package/dist/infrastructure/index.js +21 -2
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +108 -1
- package/dist/models/models-dev-schema.d.ts +308 -0
- package/dist/plugin/index.js +52 -55
- package/dist/plugins/auto-review-plugin.d.ts +18 -13
- package/dist/security/auto-approve-policy.d.ts +37 -0
- package/dist/security/index.js +64 -60
- package/dist/security/permission-helpers.d.ts +93 -0
- package/dist/security/permission-policy.d.ts +2 -52
- package/dist/storage/index.js +21 -2
- package/dist/tools/index.js +30 -2
- package/dist/types/config/autonomy.d.ts +3 -4
- package/dist/types/config/providers.d.ts +13 -1
- package/dist/types/models-registry.d.ts +9 -4
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +46 -0
- package/dist/utils/sage-output-block.js +48 -0
- package/instructions/llm/chimera-review.md +4 -6
- package/package.json +9 -4
- package/skills/auto-review/SKILL.md +11 -76
- package/skills/chimera/SKILL.md +23 -53
|
@@ -13220,8 +13220,16 @@ function resolveSubagentModelTarget(config, role, opts = {}) {
|
|
|
13220
13220
|
const resolution = resolveModelMatrixResolution(config.modelMatrix, role);
|
|
13221
13221
|
const matrixTarget = resolveModelTargetFromEntry(config, resolution?.entry);
|
|
13222
13222
|
const implementationTarget = opts.implementationTarget ?? resolveImplementationModelTarget(config);
|
|
13223
|
+
const isAvailable = (providerId, model) => {
|
|
13224
|
+
if (!opts.statusTracker) return true;
|
|
13225
|
+
if (!providerId || !model) return true;
|
|
13226
|
+
return opts.statusTracker.isAvailable(providerId, model);
|
|
13227
|
+
};
|
|
13228
|
+
const matrixEffective = materializeTarget(config, matrixTarget);
|
|
13229
|
+
const matrixIsAvailable = !matrixEffective || isAvailable(matrixEffective.provider, matrixEffective.model);
|
|
13223
13230
|
if (!roleNeedsIndependentReviewModel(role)) {
|
|
13224
13231
|
if (!matrixTarget) return void 0;
|
|
13232
|
+
if (!matrixIsAvailable) return void 0;
|
|
13225
13233
|
return {
|
|
13226
13234
|
...matrixTarget,
|
|
13227
13235
|
source: "matrix",
|
|
@@ -13230,16 +13238,27 @@ function resolveSubagentModelTarget(config, role, opts = {}) {
|
|
|
13230
13238
|
}
|
|
13231
13239
|
const matrixRef = materializeTarget(config, matrixTarget);
|
|
13232
13240
|
if (resolution?.source === "role" || resolution?.source === "phase") {
|
|
13233
|
-
|
|
13241
|
+
if (!matrixTarget) return void 0;
|
|
13242
|
+
if (!matrixIsAvailable) return void 0;
|
|
13243
|
+
return {
|
|
13244
|
+
...matrixTarget,
|
|
13245
|
+
source: "matrix",
|
|
13246
|
+
matrixSource: resolution.source
|
|
13247
|
+
};
|
|
13234
13248
|
}
|
|
13235
13249
|
if (matrixRef && !sameModelReference(matrixRef, implementationTarget)) {
|
|
13250
|
+
if (!matrixIsAvailable) return void 0;
|
|
13236
13251
|
return {
|
|
13237
13252
|
...matrixTarget ?? {},
|
|
13238
13253
|
source: "matrix",
|
|
13239
13254
|
matrixSource: resolution?.source
|
|
13240
13255
|
};
|
|
13241
13256
|
}
|
|
13242
|
-
const diverse =
|
|
13257
|
+
const diverse = chooseDiverseModelTargetWithTracker(
|
|
13258
|
+
config,
|
|
13259
|
+
implementationTarget,
|
|
13260
|
+
opts.statusTracker
|
|
13261
|
+
);
|
|
13243
13262
|
if (diverse) {
|
|
13244
13263
|
return {
|
|
13245
13264
|
provider: diverse.provider,
|
|
@@ -13253,6 +13272,7 @@ function resolveSubagentModelTarget(config, role, opts = {}) {
|
|
|
13253
13272
|
};
|
|
13254
13273
|
}
|
|
13255
13274
|
if (!matrixTarget) return void 0;
|
|
13275
|
+
if (!matrixIsAvailable) return void 0;
|
|
13256
13276
|
return {
|
|
13257
13277
|
...matrixTarget,
|
|
13258
13278
|
source: "matrix",
|
|
@@ -13289,6 +13309,14 @@ function chooseDiverseModelTarget(config, avoid) {
|
|
|
13289
13309
|
candidates.sort((a, b) => modelDiversityScore(b, avoid) - modelDiversityScore(a, avoid));
|
|
13290
13310
|
return candidates[0];
|
|
13291
13311
|
}
|
|
13312
|
+
function chooseDiverseModelTargetWithTracker(config, avoid, tracker) {
|
|
13313
|
+
if (!tracker) return chooseDiverseModelTarget(config, avoid);
|
|
13314
|
+
const ranked = collectConfiguredModelTargets(config).filter((candidate) => !sameModelReference(candidate, avoid)).sort((a, b) => modelDiversityScore(b, avoid) - modelDiversityScore(a, avoid));
|
|
13315
|
+
for (const candidate of ranked) {
|
|
13316
|
+
if (tracker.isAvailable(candidate.provider, candidate.model)) return candidate;
|
|
13317
|
+
}
|
|
13318
|
+
return void 0;
|
|
13319
|
+
}
|
|
13292
13320
|
function collectConfiguredModelTargets(config) {
|
|
13293
13321
|
const seen = /* @__PURE__ */ new Set();
|
|
13294
13322
|
const out = [];
|
|
@@ -20402,6 +20430,36 @@ function isMailboxLeader(agentId, role) {
|
|
|
20402
20430
|
function isMailboxMessageVisibleTo(message, agentId, role) {
|
|
20403
20431
|
return message.audience !== "leaders" || isMailboxLeader(agentId, role);
|
|
20404
20432
|
}
|
|
20433
|
+
function isAffectedBySessionAffinity(message) {
|
|
20434
|
+
return message.sessionAffinity !== void 0;
|
|
20435
|
+
}
|
|
20436
|
+
async function acceptMailboxMessageForSession(message, currentSessionId, ctx) {
|
|
20437
|
+
if (!isAffectedBySessionAffinity(message)) return true;
|
|
20438
|
+
const affinity = message.sessionAffinity;
|
|
20439
|
+
if (affinity === null || typeof affinity !== "object" || Array.isArray(affinity)) {
|
|
20440
|
+
return false;
|
|
20441
|
+
}
|
|
20442
|
+
if (affinity.sessionId !== void 0 && typeof affinity.sessionId !== "string" || affinity.reportId !== void 0 && typeof affinity.reportId !== "string") {
|
|
20443
|
+
return false;
|
|
20444
|
+
}
|
|
20445
|
+
if (!currentSessionId) {
|
|
20446
|
+
return ctx?.allowUnscoped === true;
|
|
20447
|
+
}
|
|
20448
|
+
if (typeof affinity.sessionId === "string" && affinity.sessionId.length > 0) {
|
|
20449
|
+
if (affinity.sessionId !== currentSessionId) return false;
|
|
20450
|
+
return true;
|
|
20451
|
+
}
|
|
20452
|
+
if (affinity.reportId && ctx?.resolveChimeraReportSessionId) {
|
|
20453
|
+
try {
|
|
20454
|
+
const resolved = await ctx.resolveChimeraReportSessionId(affinity.reportId);
|
|
20455
|
+
if (resolved === currentSessionId) return true;
|
|
20456
|
+
if (resolved !== void 0) return false;
|
|
20457
|
+
} catch {
|
|
20458
|
+
}
|
|
20459
|
+
}
|
|
20460
|
+
if (ctx?.allowUnscoped === true) return true;
|
|
20461
|
+
return false;
|
|
20462
|
+
}
|
|
20405
20463
|
function validateSendType(type, to) {
|
|
20406
20464
|
if (type === "control") {
|
|
20407
20465
|
throw new TypeError(
|
|
@@ -21025,7 +21083,7 @@ function makeMailboxTool(opts = {}) {
|
|
|
21025
21083
|
case "ack":
|
|
21026
21084
|
return executeAck(mb, callerId, i);
|
|
21027
21085
|
case "query":
|
|
21028
|
-
return executeQuery(mb, callerId, identity.role, i);
|
|
21086
|
+
return executeQuery(mb, callerId, callerSessionId, identity.role, i);
|
|
21029
21087
|
case "status":
|
|
21030
21088
|
return executeStatus(mb);
|
|
21031
21089
|
case "online":
|
|
@@ -21054,12 +21112,18 @@ async function executeCheck(mb, agentId, sessionId, aliases, role, i) {
|
|
|
21054
21112
|
)
|
|
21055
21113
|
);
|
|
21056
21114
|
const seen = /* @__PURE__ */ new Set();
|
|
21057
|
-
const
|
|
21115
|
+
const candidates = batches.flat().filter((m) => {
|
|
21058
21116
|
if (seen.has(m.id)) return false;
|
|
21059
21117
|
if (!isMailboxMessageVisibleTo(m, agentId, role)) return false;
|
|
21060
21118
|
seen.add(m.id);
|
|
21061
21119
|
return true;
|
|
21062
21120
|
});
|
|
21121
|
+
const messages = [];
|
|
21122
|
+
for (const m of candidates) {
|
|
21123
|
+
if (await acceptMailboxMessageForSession(m, sessionId)) {
|
|
21124
|
+
messages.push(m);
|
|
21125
|
+
}
|
|
21126
|
+
}
|
|
21063
21127
|
const acked = markRead || completed ? await mb.ackMany({
|
|
21064
21128
|
acks: messages.map((m) => ({
|
|
21065
21129
|
messageId: m.id,
|
|
@@ -21140,7 +21204,7 @@ async function executeAck(mb, agentId, i) {
|
|
|
21140
21204
|
summary: `Message ${messageId} acknowledged. Read by ${Object.keys(updated.readBy).length} agent(s), Completed: ${updated.completed}.`
|
|
21141
21205
|
};
|
|
21142
21206
|
}
|
|
21143
|
-
async function executeQuery(mb, agentId, role, i) {
|
|
21207
|
+
async function executeQuery(mb, agentId, sessionId, role, i) {
|
|
21144
21208
|
const limit = i.limit ?? 50;
|
|
21145
21209
|
const messages = await mb.query({
|
|
21146
21210
|
to: i.to,
|
|
@@ -21153,7 +21217,12 @@ async function executeQuery(mb, agentId, role, i) {
|
|
|
21153
21217
|
sessionId: i.sessionId,
|
|
21154
21218
|
limit
|
|
21155
21219
|
});
|
|
21156
|
-
const visible =
|
|
21220
|
+
const visible = [];
|
|
21221
|
+
for (const message of messages) {
|
|
21222
|
+
if (isMailboxMessageVisibleTo(message, agentId, role) && await acceptMailboxMessageForSession(message, sessionId)) {
|
|
21223
|
+
visible.push(message);
|
|
21224
|
+
}
|
|
21225
|
+
}
|
|
21157
21226
|
return { ok: true, count: visible.length, messages: visible, summary: `${visible.length} message(s).` };
|
|
21158
21227
|
}
|
|
21159
21228
|
async function executeStatus(mb) {
|
|
@@ -21944,6 +22013,14 @@ var SEND_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
|
21944
22013
|
"senderSessionId",
|
|
21945
22014
|
"ttlMs",
|
|
21946
22015
|
"taskContext"
|
|
22016
|
+
// 'sessionAffinity' is NOT in the allow-list. The trust-line contract:
|
|
22017
|
+
// session-affinity tokens are stamped ONLY by trusted internal callers
|
|
22018
|
+
// (chimera/auto-review pipelines) that call `mailbox.send()` directly
|
|
22019
|
+
// and never go through this boundary codec. Allowing the field at the
|
|
22020
|
+
// boundary would let any actor with `mail.send.informational` stamp
|
|
22021
|
+
// another session's id and bypass the receiver-side filter. The
|
|
22022
|
+
// receiver trusts the sender-asserted `sessionId`; the boundary must
|
|
22023
|
+
// refuse the field entirely.
|
|
21947
22024
|
]);
|
|
21948
22025
|
var ACK_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
21949
22026
|
"messageId",
|
|
@@ -21980,7 +22057,15 @@ function parseMailboxSendInput(payload, actor) {
|
|
|
21980
22057
|
const priority = validatePriority(rawPriority);
|
|
21981
22058
|
const audience = validateAudience(payload["audience"]);
|
|
21982
22059
|
const replyTo = optionalString2(payload, "replyTo", "send");
|
|
21983
|
-
return {
|
|
22060
|
+
return {
|
|
22061
|
+
to,
|
|
22062
|
+
type: typeResult.type,
|
|
22063
|
+
subject,
|
|
22064
|
+
body,
|
|
22065
|
+
priority,
|
|
22066
|
+
audience,
|
|
22067
|
+
replyTo
|
|
22068
|
+
};
|
|
21984
22069
|
}
|
|
21985
22070
|
function parseMailboxQueryInput(payload, actor) {
|
|
21986
22071
|
assertCapability(actor, "mail.read.self", "query");
|
|
@@ -75,12 +75,12 @@ export declare function makeMailSendTool(opts?: MailToolsOptions): {
|
|
|
75
75
|
required: string[];
|
|
76
76
|
};
|
|
77
77
|
execute(input: unknown, ctx: Context): Promise<{
|
|
78
|
-
messageId?: never;
|
|
79
|
-
to?: never;
|
|
80
|
-
summary?: never;
|
|
81
78
|
ok: boolean;
|
|
82
79
|
error: string;
|
|
80
|
+
messageId?: never;
|
|
83
81
|
from?: never;
|
|
82
|
+
to?: never;
|
|
83
|
+
summary?: never;
|
|
84
84
|
} | {
|
|
85
85
|
error?: never;
|
|
86
86
|
ok: boolean;
|
|
@@ -56,7 +56,7 @@ export declare function parseMailboxSendInput(payload: Record<string, unknown>,
|
|
|
56
56
|
* Queries tolerate unknown fields (forward compatibility for read-only clients).
|
|
57
57
|
* Actor-derived recipient forms are NOT overridden by body-supplied `readerRole`.
|
|
58
58
|
*
|
|
59
|
-
* @throws {MailboxValidationError} on validation failure.
|
|
59
|
+
* @throws {MailboxValidationError} on any validation failure.
|
|
60
60
|
*/
|
|
61
61
|
export declare function parseMailboxQueryInput(payload: Record<string, unknown>, actor: MailboxActorContext): MailboxQuery;
|
|
62
62
|
/**
|
|
@@ -1520,6 +1520,34 @@ function parseTaskContext(value) {
|
|
|
1520
1520
|
...status === void 0 ? {} : { status }
|
|
1521
1521
|
};
|
|
1522
1522
|
}
|
|
1523
|
+
function parseSessionAffinity(value) {
|
|
1524
|
+
if (value === void 0) return void 0;
|
|
1525
|
+
if (!isRecord2(value)) {
|
|
1526
|
+
throw new TypeError('mailbox message field "sessionAffinity" must be an object');
|
|
1527
|
+
}
|
|
1528
|
+
const sessionId = value["sessionId"];
|
|
1529
|
+
const reportId = value["reportId"];
|
|
1530
|
+
const kind = optionalString(value, "kind");
|
|
1531
|
+
if (sessionId !== void 0) {
|
|
1532
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
1533
|
+
throw new TypeError("mailbox message sessionAffinity.sessionId must be a non-empty string");
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
sessionId,
|
|
1537
|
+
...optionalString(value, "reportId"),
|
|
1538
|
+
...kind
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
if (typeof reportId !== "string" || reportId.length === 0) {
|
|
1542
|
+
throw new TypeError(
|
|
1543
|
+
"mailbox message sessionAffinity.reportId must be a non-empty string when sessionId is absent"
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
reportId,
|
|
1548
|
+
...kind
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1523
1551
|
function parseMailboxMessage(value) {
|
|
1524
1552
|
if (!isRecord2(value)) throw new TypeError("mailbox message must be an object");
|
|
1525
1553
|
const to = value["to"] === void 0 ? "" : requiredString(value, "to");
|
|
@@ -1529,6 +1557,7 @@ function parseMailboxMessage(value) {
|
|
|
1529
1557
|
}
|
|
1530
1558
|
const taskContext = parseTaskContext(value["taskContext"]);
|
|
1531
1559
|
const audience = parseAudience(value["audience"]);
|
|
1560
|
+
const sessionAffinity = parseSessionAffinity(value["sessionAffinity"]);
|
|
1532
1561
|
return {
|
|
1533
1562
|
id: requiredString(value, "id"),
|
|
1534
1563
|
from: requiredString(value, "from"),
|
|
@@ -1549,7 +1578,8 @@ function parseMailboxMessage(value) {
|
|
|
1549
1578
|
...optionalString(value, "replyTo"),
|
|
1550
1579
|
...optionalString(value, "senderSessionId"),
|
|
1551
1580
|
...optionalString(value, "expiresAt"),
|
|
1552
|
-
...taskContext === void 0 ? {} : { taskContext }
|
|
1581
|
+
...taskContext === void 0 ? {} : { taskContext },
|
|
1582
|
+
...sessionAffinity === void 0 ? {} : { sessionAffinity }
|
|
1553
1583
|
};
|
|
1554
1584
|
}
|
|
1555
1585
|
function isAckRecord(value) {
|
|
@@ -2620,6 +2650,7 @@ var SqliteMailbox = class {
|
|
|
2620
2650
|
...input.replyTo !== void 0 ? { replyTo: input.replyTo } : {},
|
|
2621
2651
|
...input.taskContext !== void 0 ? { taskContext: input.taskContext } : {},
|
|
2622
2652
|
...input.senderSessionId !== void 0 ? { senderSessionId: input.senderSessionId } : {},
|
|
2653
|
+
...input.sessionAffinity !== void 0 ? { sessionAffinity: input.sessionAffinity } : {},
|
|
2623
2654
|
...input.ttlMs !== void 0 ? { expiresAt: new Date(Date.now() + input.ttlMs).toISOString() } : {}
|
|
2624
2655
|
};
|
|
2625
2656
|
this.persistMessage(message);
|
|
@@ -88,7 +88,7 @@ export { MAILBOX_TYPE_PROPERTIES, type MailboxMessageType, type MailboxTypeCateg
|
|
|
88
88
|
* The mailbox system enforces these dispatch rules:
|
|
89
89
|
*
|
|
90
90
|
* 1. **Send-side**: `mail_send` auto-defaults the type: `broadcast` when
|
|
91
|
-
* `to` is `"*"` or `"@session
|
|
91
|
+
* `to` is `"*"` or `"@session:..."`, otherwise `note`.
|
|
92
92
|
* 2. **Send-side validation**: `assign` always requires a specific `to`
|
|
93
93
|
* (not `"*"`). `control` is reserved for runtime use — agents passing it
|
|
94
94
|
* via the tool surface will be rejected.
|
|
@@ -98,8 +98,8 @@ export { MAILBOX_TYPE_PROPERTIES, type MailboxMessageType, type MailboxTypeCateg
|
|
|
98
98
|
* 4. **Control isolation**: `control`-type messages are filtered by
|
|
99
99
|
* `injectPendingMailboxMessages()` and NEVER enter the folded
|
|
100
100
|
* conversation block — they are out-of-band signals only.
|
|
101
|
-
* 5. **Background routing**: in `background` delivery mode, only
|
|
102
|
-
*
|
|
101
|
+
* 5. **Background routing**: in `background` delivery mode, only the
|
|
102
|
+
* actionable message types (`steer`, `ask`, `assign`, `result`,
|
|
103
103
|
* `review`) are escalated; `note`, `btw`, `status`, and `broadcast`
|
|
104
104
|
* are suppressed to minimise disruption during tool work.
|
|
105
105
|
* 6. **Awareness polling**: `btw` messages intercepted by background
|
|
@@ -134,6 +134,100 @@ export declare function mailboxIdentityBase(agentId: string): string;
|
|
|
134
134
|
export declare function isMailboxLeader(agentId: string, role?: string): boolean;
|
|
135
135
|
/** Whether a message may be consumed by the supplied agent identity. */
|
|
136
136
|
export declare function isMailboxMessageVisibleTo(message: Pick<MailboxMessage, 'audience'>, agentId: string, role?: string): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Shape of the optional secondary lookup the leader filter performs when a
|
|
139
|
+
* message arrives without an affinity token (legacy sender / older build).
|
|
140
|
+
*
|
|
141
|
+
* `resolveChimeraReportSessionId(reportId)` returns the persisted
|
|
142
|
+
* `ReviewReport.sessionId` for that report id, or `undefined` when the
|
|
143
|
+
* report is unknown. May be sync or async — the filter awaits the result.
|
|
144
|
+
*/
|
|
145
|
+
export interface MailboxSessionAffinityContext {
|
|
146
|
+
/**
|
|
147
|
+
* Look up the originating session id of a chimera report by its report id.
|
|
148
|
+
* Return `undefined` for unknown reports. Sync or async.
|
|
149
|
+
*/
|
|
150
|
+
resolveChimeraReportSessionId?: ((reportId: string) => string | undefined | Promise<string | undefined>) | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Legacy / one-shot opt-in. The flag has TWO consultation points inside
|
|
153
|
+
* {@link acceptMailboxMessageForSession}:
|
|
154
|
+
*
|
|
155
|
+
* 1. When the recipient does not pass a `currentSessionId` (rule 2),
|
|
156
|
+
* a token-bearing message is accepted iff this is `true`.
|
|
157
|
+
* 2. When a token-bearing message lacks a resolvable `sessionId` AND
|
|
158
|
+
* the persisted-report resolver returns no match (rule 5), the
|
|
159
|
+
* message is accepted iff this is `true`.
|
|
160
|
+
*
|
|
161
|
+
* Token-LESS messages are NOT gated by this flag — they are accepted
|
|
162
|
+
* unconditionally by rule 1 as the discriminator for legitimate
|
|
163
|
+
* subagent `result` / `review` traffic that does not stamp an affinity
|
|
164
|
+
* token (task-auctioneer, cascade handlers, legacy senders).
|
|
165
|
+
*
|
|
166
|
+
* This flag does NOT override an explicit mismatch: a message that
|
|
167
|
+
* carries `sessionAffinity.sessionId !== currentSessionId` is dropped
|
|
168
|
+
* regardless of this flag, because the sender explicitly claimed a
|
|
169
|
+
* different session — honoring it would re-open the leak.
|
|
170
|
+
*
|
|
171
|
+
* Default `false`.
|
|
172
|
+
*/
|
|
173
|
+
allowUnscoped?: boolean | undefined;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Decide whether a mailbox message belongs to the recipient's current session
|
|
177
|
+
* and should therefore be delivered to the leader's inbox.
|
|
178
|
+
*
|
|
179
|
+
* The actual code path is:
|
|
180
|
+
*
|
|
181
|
+
* 1. {@link isAffectedBySessionAffinity} check. If the message has no
|
|
182
|
+
* affinity token, the filter does NOT apply and the message is accepted.
|
|
183
|
+
* This is the **discriminator** that protects legitimate subagent
|
|
184
|
+
* `result` / `review` traffic (task-auctioneer, cascade handlers) from
|
|
185
|
+
* collateral drops — they do not stamp an affinity token, so they fall
|
|
186
|
+
* through here. When a token is present, every token is enforced regardless
|
|
187
|
+
* of its optional `kind`; malformed or unrecognized kinds must not bypass
|
|
188
|
+
* a mismatched `sessionAffinity.sessionId`.
|
|
189
|
+
* 2. No-session-id branch. If the recipient did not pass a `currentSessionId`,
|
|
190
|
+
* accept only when `ctx.allowUnscoped === true`. Default: **fail-closed**
|
|
191
|
+
* (drop) — the safe behavior, not a silent fail-open.
|
|
192
|
+
* 3. Explicit `sessionAffinity.sessionId` match. If the sender stamped a
|
|
193
|
+
* session id and it does not match the recipient's current session id,
|
|
194
|
+
* drop immediately. The sender claimed a different session; honoring it
|
|
195
|
+
* would re-open the cross-session leak. This drop happens even when
|
|
196
|
+
* `allowUnscoped` is set — a wrong session is misconfiguration, not a
|
|
197
|
+
* legacy case.
|
|
198
|
+
* 4. Persisted-report fallback. If the token lacks a `sessionId` but carries
|
|
199
|
+
* a `reportId`, await `ctx.resolveChimeraReportSessionId(reportId)` and
|
|
200
|
+
* accept if the persisted `ReviewReport.sessionId` matches. Accepts
|
|
201
|
+
* sync or async resolvers.
|
|
202
|
+
* 5. `allowUnscoped` opt-in. If the previous steps did not accept and
|
|
203
|
+
* `ctx.allowUnscoped === true`, accept (legacy / one-shot tool
|
|
204
|
+
* compatibility).
|
|
205
|
+
* 6. Otherwise drop (`false`).
|
|
206
|
+
*
|
|
207
|
+
* Trust note: the filter accepts the sender-asserted `sessionAffinity.sessionId`
|
|
208
|
+
* without store cross-check (rule 3). The persisted-report fallback (rule 4)
|
|
209
|
+
* is the only server-side check. A sender that fabricates both an affinity
|
|
210
|
+
* token and a persisted report could impersonate another session — guard
|
|
211
|
+
* chimera's send path so only the actual originating session can stamp the
|
|
212
|
+
* token, and do not relax rule 3.
|
|
213
|
+
*
|
|
214
|
+
* The helper is side-effect-free except for awaiting the optional resolver.
|
|
215
|
+
* It lives next to {@link isMailboxMessageVisibleTo} so mailbox consumers
|
|
216
|
+
* can compose both filters without taking a dependency on the persistence
|
|
217
|
+
* layer.
|
|
218
|
+
*/
|
|
219
|
+
export declare function acceptMailboxMessageForSession(message: Pick<MailboxMessage, 'type' | 'sessionAffinity'>, currentSessionId: string | undefined, ctx?: MailboxSessionAffinityContext): Promise<boolean>;
|
|
220
|
+
/**
|
|
221
|
+
* Synchronous wrapper for callers that cannot await the resolver
|
|
222
|
+
* (e.g. legacy event handlers). If `ctx.resolveChimeraReportSessionId` is
|
|
223
|
+
* async, it is called without awaiting and the result is ignored — the
|
|
224
|
+
* rule degrades to "match affinity.sessionId, otherwise drop unless
|
|
225
|
+
* allowUnscoped".
|
|
226
|
+
*
|
|
227
|
+
* Prefer {@link acceptMailboxMessageForSession} whenever the caller can
|
|
228
|
+
* await.
|
|
229
|
+
*/
|
|
230
|
+
export declare function acceptMailboxMessageForSessionSync(message: Pick<MailboxMessage, 'type' | 'sessionAffinity'>, currentSessionId: string | undefined, ctx?: MailboxSessionAffinityContext): boolean;
|
|
137
231
|
/** Category + expectsReply are provided by MAILBOX_TYPE_PROPERTIES directly. */
|
|
138
232
|
/**
|
|
139
233
|
* Validate that a given (type, to) pair is internally consistent.
|
|
@@ -196,6 +290,13 @@ export interface MailboxMessage {
|
|
|
196
290
|
taskContext?: MailboxTaskContext | undefined;
|
|
197
291
|
/** Session id of the sender. Enables cross-session communication. */
|
|
198
292
|
senderSessionId?: string | undefined;
|
|
293
|
+
/**
|
|
294
|
+
* Session-affinity token persisted on the message. When set, the
|
|
295
|
+
* recipient's leader filter uses it to drop the message if the
|
|
296
|
+
* recipient's current session id does not match. See
|
|
297
|
+
* {@link MailboxSessionAffinity} for the contract.
|
|
298
|
+
*/
|
|
299
|
+
sessionAffinity?: MailboxSessionAffinity | undefined;
|
|
199
300
|
/**
|
|
200
301
|
* ISO8601 — when the message expires. Set at send time from `ttlMs`
|
|
201
302
|
* (default: 24h via AUTO_COMPACT_DEFAULT_TTL_MS). The auto-compaction
|
|
@@ -352,6 +453,25 @@ export interface MailboxSendInput {
|
|
|
352
453
|
taskContext?: MailboxTaskContext | undefined;
|
|
353
454
|
/** Sender session id. Required when `to` is the `"@session"` alias. */
|
|
354
455
|
senderSessionId?: string | undefined;
|
|
456
|
+
/**
|
|
457
|
+
* Session-affinity token. When set, the recipient's mailbox filter uses it
|
|
458
|
+
* to drop the message if the recipient's current session id does not match.
|
|
459
|
+
*
|
|
460
|
+
* Why two fields instead of one: a chimera report is generated by a reviewer
|
|
461
|
+
* subagent that runs inside the originating session (so `senderSessionId`
|
|
462
|
+
* is correct), but a mailbox envelope may also be relayed by an outer
|
|
463
|
+
* pipeline (auto-review cascade, broadcast wrapper) where the recipient
|
|
464
|
+
* needs to know which session the report *belongs to*, not who *physically
|
|
465
|
+
* called send()*. `sessionId` carries the originating session;
|
|
466
|
+
* `reportId` carries the chimera report id so the leader's filter
|
|
467
|
+
* can also look the report up in the persisted review-report store.
|
|
468
|
+
*
|
|
469
|
+
* A session-affinity token is **required** for any `chimera.*` review
|
|
470
|
+
* delivery wrapper — this is the trust boundary that prevents a leader
|
|
471
|
+
* from acting on reports emitted by another session, even when those
|
|
472
|
+
* reports land in its mailbox.
|
|
473
|
+
*/
|
|
474
|
+
sessionAffinity?: MailboxSessionAffinity | undefined;
|
|
355
475
|
/**
|
|
356
476
|
* Time-to-live in milliseconds. When set, the message's `expiresAt` is
|
|
357
477
|
* computed as `now + ttlMs` at send time. The auto-compaction sweep
|
|
@@ -359,6 +479,41 @@ export interface MailboxSendInput {
|
|
|
359
479
|
*/
|
|
360
480
|
ttlMs?: number | undefined;
|
|
361
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Session-affinity token carried on a mailbox message.
|
|
484
|
+
*
|
|
485
|
+
* The token is the *only* signal a recipient's leader filter uses to decide
|
|
486
|
+
* whether a chimera report belongs to the recipient's own session. Without
|
|
487
|
+
* it, any leader that polls the project-wide mailbox would see — and feel
|
|
488
|
+
* compelled to act on — chimera reports emitted by every other session in
|
|
489
|
+
* the project.
|
|
490
|
+
*/
|
|
491
|
+
export type MailboxSessionAffinity = MailboxScopedSessionAffinity | MailboxLegacyReportSessionAffinity;
|
|
492
|
+
export interface MailboxScopedSessionAffinity {
|
|
493
|
+
/**
|
|
494
|
+
* The originating session id. The recipient's leader filter compares this
|
|
495
|
+
* against its own current session id and drops the message on mismatch.
|
|
496
|
+
*/
|
|
497
|
+
sessionId: string;
|
|
498
|
+
/**
|
|
499
|
+
* Originating chimera report id (UUID). Used as a secondary lookup only
|
|
500
|
+
* when the message is a legacy report token without `sessionId`.
|
|
501
|
+
*/
|
|
502
|
+
reportId?: string | undefined;
|
|
503
|
+
/** Free-form tag for filtering / metrics, e.g. `'chimera.review'`. */
|
|
504
|
+
kind?: string | undefined;
|
|
505
|
+
}
|
|
506
|
+
export interface MailboxLegacyReportSessionAffinity {
|
|
507
|
+
/**
|
|
508
|
+
* Legacy report-only tokens deliberately omit `sessionId`; any present
|
|
509
|
+
* session id is authoritative and must be checked before report lookup.
|
|
510
|
+
*/
|
|
511
|
+
sessionId?: undefined;
|
|
512
|
+
/** Originating chimera report id (UUID) used for persisted-session lookup. */
|
|
513
|
+
reportId: string;
|
|
514
|
+
/** Free-form tag for filtering / metrics, e.g. `'chimera.review'`. */
|
|
515
|
+
kind?: string | undefined;
|
|
516
|
+
}
|
|
362
517
|
/**
|
|
363
518
|
* Append-only ack record stored in the JSONL alongside messages.
|
|
364
519
|
*
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* truth both that command and the spawn path use to validate + resolve keys.
|
|
18
18
|
*/
|
|
19
19
|
import type { Config, ModelMatrixEntry } from '../types/config.js';
|
|
20
|
+
import type { ProviderModelStatusTracker } from './provider-status-tracker.js';
|
|
20
21
|
/** Either a static matrix or a live getter (re-read on every spawn). */
|
|
21
22
|
export type ModelMatrixSource = Record<string, ModelMatrixEntry> | (() => Record<string, ModelMatrixEntry> | undefined);
|
|
22
23
|
/** All valid phase keys, in catalog order. */
|
|
@@ -81,9 +82,24 @@ export declare function roleNeedsIndependentReviewModel(role: string | undefined
|
|
|
81
82
|
* implementation model.
|
|
82
83
|
* 3. A different configured provider/model for review roles.
|
|
83
84
|
* 4. The matrix/leader fallback.
|
|
85
|
+
*
|
|
86
|
+
* When a {@link ProviderModelStatusTracker} is supplied, resolved candidates
|
|
87
|
+
* currently in the waiting room (`state: 'blocked'`) are dropped so the
|
|
88
|
+
* subagent never spawns on a model the leader just 429-stricken. The
|
|
89
|
+
* fallback extension inside the subagent will also re-check `isAvailable`
|
|
90
|
+
* before invoking the provider, but filtering here saves the spawn
|
|
91
|
+
* round-trip on doomed picks.
|
|
92
|
+
*
|
|
93
|
+
* The tracker check uses the EFFECTIVE (provider, model) pair — i.e. the
|
|
94
|
+
* pair that {@link materializeTarget} would render — not the raw
|
|
95
|
+
* `matrixTarget.provider` field. A model-only matrix entry falls back to
|
|
96
|
+
* `config.provider` for the wire call, so the waiting-room entry sits on
|
|
97
|
+
* the same effective identity. Checking the raw field would silently
|
|
98
|
+
* bypass the quarantine and respawn on a 429-stricken model.
|
|
84
99
|
*/
|
|
85
100
|
export declare function resolveSubagentModelTarget(config: Config, role: string | undefined, opts?: {
|
|
86
101
|
implementationTarget?: ModelReference | undefined;
|
|
102
|
+
statusTracker?: ProviderModelStatusTracker | undefined;
|
|
87
103
|
}): ResolvedSubagentModelTarget | undefined;
|
|
88
104
|
/**
|
|
89
105
|
* Resolve the default implementation lane. The generic Executor is the closest
|