@felspar/sdk 0.1.1 → 0.2.1
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/README.md +68 -34
- package/bin/felspar-sdk.mjs +10 -0
- package/bin/skill.mjs +33 -0
- package/dist/chunks/chunk-B4BWUYQY.js +53 -0
- package/dist/chunks/{chunk-C2VMKF3S.js → chunk-XLKRU5RA.js} +1190 -829
- package/dist/chunks/chunk-Y6KRRIQI.js +908 -0
- package/dist/draft-action-gate-claude.js +7 -6
- package/dist/draft-action-gate-eve.js +5 -48
- package/dist/draft-action-gate-openai.js +7 -6
- package/dist/draft-action-gate.js +6 -2
- package/dist/draft-hosted-client.js +1 -1
- package/dist/eve.js +53 -0
- package/dist/types/draft-action-gate-cases.d.ts +31 -0
- package/dist/types/draft-action-gate-decisions.d.ts +33 -3
- package/dist/types/draft-action-gate-describe.d.ts +81 -0
- package/dist/types/draft-action-gate-rules.d.ts +15 -0
- package/dist/types/draft-action-gate-subject.d.ts +33 -0
- package/dist/types/draft-action-gate.d.ts +17 -11
- package/dist/types/draft-hosted-client.d.ts +427 -30
- package/dist/types/eve.d.ts +22 -0
- package/package.json +10 -1
- package/skills/felspar/SKILL.md +128 -0
- package/dist/chunks/chunk-6EZBI7O6.js +0 -583
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHostedFelsparClient
|
|
3
|
+
} from "./chunk-XLKRU5RA.js";
|
|
4
|
+
|
|
5
|
+
// src/draft-action-gate-outputs.ts
|
|
6
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
function parsedJson(text2) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(text2);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function replyOf(output) {
|
|
15
|
+
let value = output;
|
|
16
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
value = parsedJson(value);
|
|
19
|
+
} else if (Array.isArray(value)) {
|
|
20
|
+
value = value.find((part) => isRecord(part) && part.type === "text");
|
|
21
|
+
} else if (!isRecord(value) || value.isError === true) {
|
|
22
|
+
return null;
|
|
23
|
+
} else if (typeof value.checkId === "string") {
|
|
24
|
+
return value;
|
|
25
|
+
} else if (isRecord(value.structuredContent)) {
|
|
26
|
+
value = value.structuredContent;
|
|
27
|
+
} else {
|
|
28
|
+
value = Array.isArray(value.content) ? value.content : value.text;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
var text = (value) => typeof value === "string" ? value : null;
|
|
34
|
+
function outputCheckFromResult(jobId, output) {
|
|
35
|
+
const reply = replyOf(output);
|
|
36
|
+
const receipt = isRecord(reply?.receipt) ? reply.receipt : null;
|
|
37
|
+
const decision = isRecord(reply?.decision) ? reply.decision : null;
|
|
38
|
+
const fields = {
|
|
39
|
+
checkId: text(reply?.checkId),
|
|
40
|
+
output: text(reply?.output),
|
|
41
|
+
decisionReceiptDigest: text(decision?.receiptDigest),
|
|
42
|
+
artifactDigest: text(receipt?.artifactDigest),
|
|
43
|
+
receiptDigest: text(receipt?.receiptDigest)
|
|
44
|
+
};
|
|
45
|
+
if (reply?.status !== "pass" || Object.values(fields).includes(null)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
checkId: fields.checkId ?? "",
|
|
50
|
+
jobId,
|
|
51
|
+
output: fields.output ?? "",
|
|
52
|
+
status: "pass",
|
|
53
|
+
decisionReceiptDigest: fields.decisionReceiptDigest ?? "",
|
|
54
|
+
artifactDigest: fields.artifactDigest ?? "",
|
|
55
|
+
receiptDigest: fields.receiptDigest ?? ""
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function outputTextDigest(value) {
|
|
59
|
+
const encoder = new TextEncoder();
|
|
60
|
+
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
61
|
+
return `sha256:${Array.from(hash, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
62
|
+
}
|
|
63
|
+
function checkedText(rule, input) {
|
|
64
|
+
const wanted = new Set(rule.fields.map((field) => field.toLowerCase()));
|
|
65
|
+
const values = Object.entries(input).filter(([field]) => wanted.has(field.toLowerCase())).map(([, value]) => value);
|
|
66
|
+
const [only] = values;
|
|
67
|
+
return typeof only === "string" && values.length === 1 ? only : null;
|
|
68
|
+
}
|
|
69
|
+
async function outputCheckProblem(rule, call) {
|
|
70
|
+
const value = checkedText(rule, call.input);
|
|
71
|
+
if (value === null) {
|
|
72
|
+
return `the call must set exactly one text field of ${rule.fields.join(", ")}`;
|
|
73
|
+
}
|
|
74
|
+
const digest = await outputTextDigest(value);
|
|
75
|
+
let check;
|
|
76
|
+
for (const row of call.checks) {
|
|
77
|
+
const isOutput = rule.output === void 0 || row.output === rule.output;
|
|
78
|
+
const isText2 = row.artifactDigest === digest;
|
|
79
|
+
const isDecision = row.decisionReceiptDigest === call.decisionReceiptDigest;
|
|
80
|
+
const isPass = row.status === "pass";
|
|
81
|
+
if (isPass && isText2 && isDecision && isOutput) {
|
|
82
|
+
check = row;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (check === void 0) {
|
|
86
|
+
return "Felspar has not passed this exact text for this decision; call check_output first";
|
|
87
|
+
}
|
|
88
|
+
if (call.verify === void 0) {
|
|
89
|
+
return "no output-check verifier is configured";
|
|
90
|
+
}
|
|
91
|
+
const confirmed = await call.verify(check);
|
|
92
|
+
const isConfirmed = confirmed?.status === "pass" && confirmed.receiptDigest === check.receiptDigest && confirmed.artifactDigest === check.artifactDigest && confirmed.decisionReceiptDigest === check.decisionReceiptDigest;
|
|
93
|
+
return isConfirmed ? null : "Felspar did not confirm the output check";
|
|
94
|
+
}
|
|
95
|
+
function felsparOutputCheckVerifier(configuration) {
|
|
96
|
+
let client;
|
|
97
|
+
return async (check) => {
|
|
98
|
+
client ??= createHostedFelsparClient(configuration);
|
|
99
|
+
const kept = await client.readOutputCheck(check.jobId, check.checkId);
|
|
100
|
+
return {
|
|
101
|
+
status: kept.status,
|
|
102
|
+
receiptDigest: kept.receipt?.receiptDigest ?? null,
|
|
103
|
+
artifactDigest: kept.receipt?.artifactDigest ?? null,
|
|
104
|
+
decisionReceiptDigest: kept.decision.receiptDigest
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/draft-action-gate-decisions.ts
|
|
110
|
+
var identityModes = ["provided", "fields", "content", "issued"];
|
|
111
|
+
var escalationTriggers = ["outcome", "unresolved", "attempt_limit"];
|
|
112
|
+
var isIdentityMode = (value) => identityModes.includes(value);
|
|
113
|
+
var isEscalationTrigger = (value) => escalationTriggers.includes(value);
|
|
114
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
function parsedJson2(text2) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(text2);
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function innerValue(value) {
|
|
123
|
+
if (typeof value === "string") {
|
|
124
|
+
return parsedJson2(value);
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
return value.find((part) => isRecord2(part) && part.type === "text") ?? null;
|
|
128
|
+
}
|
|
129
|
+
if (!isRecord2(value)) {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
if (value.isError === true) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (isRecord2(value.structuredContent)) {
|
|
136
|
+
return value.structuredContent;
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value.content)) {
|
|
139
|
+
return value.content;
|
|
140
|
+
}
|
|
141
|
+
return value.type === "text" && typeof value.text === "string" ? value.text : void 0;
|
|
142
|
+
}
|
|
143
|
+
function jobResponse(output) {
|
|
144
|
+
let value = output;
|
|
145
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
146
|
+
const inner = innerValue(value);
|
|
147
|
+
if (inner === void 0) {
|
|
148
|
+
return isRecord2(value) && isRecord2(value.result) ? value : null;
|
|
149
|
+
}
|
|
150
|
+
value = inner;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function jobOf(input) {
|
|
155
|
+
return isRecord2(input) && typeof input.job === "string" ? input.job : void 0;
|
|
156
|
+
}
|
|
157
|
+
function entityIdOf(input) {
|
|
158
|
+
return isRecord2(input) && typeof input.entity_id === "string" ? input.entity_id : void 0;
|
|
159
|
+
}
|
|
160
|
+
function freshnessOf(explanation) {
|
|
161
|
+
return isRecord2(explanation) && typeof explanation.evidenceFreshUntil === "string" ? explanation.evidenceFreshUntil : null;
|
|
162
|
+
}
|
|
163
|
+
var isId = (value) => typeof value === "string" && value.length > 0 && value.length <= 200;
|
|
164
|
+
function caseOf(result) {
|
|
165
|
+
if (!Object.hasOwn(result, "case")) {
|
|
166
|
+
return void 0;
|
|
167
|
+
}
|
|
168
|
+
const found = result.case;
|
|
169
|
+
return isRecord2(found) && isId(found.id) && isIdentityMode(found.mode) ? { id: found.id, mode: found.mode } : null;
|
|
170
|
+
}
|
|
171
|
+
function escalationOf(result) {
|
|
172
|
+
const found = result.escalation;
|
|
173
|
+
if (!isRecord2(found) || !isId(found.id) || !isEscalationTrigger(found.trigger)) {
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
const why = isRecord2(found.why) && typeof found.why.text === "string" ? found.why.text : "";
|
|
177
|
+
return { id: found.id, trigger: found.trigger, why };
|
|
178
|
+
}
|
|
179
|
+
var requestOf = (request) => typeof request === "string" ? { entity: request } : request;
|
|
180
|
+
function decidedOf(result) {
|
|
181
|
+
const { receipt, outcome, executionObjectId } = result;
|
|
182
|
+
if (typeof outcome !== "string" || typeof executionObjectId !== "string" || !isRecord2(receipt)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const { receiptDigest } = receipt;
|
|
186
|
+
return typeof receiptDigest === "string" ? { outcome, executionObjectId, receipt: { ...receipt, receiptDigest } } : null;
|
|
187
|
+
}
|
|
188
|
+
function decisionFromResult(request, output) {
|
|
189
|
+
const result = jobResponse(output)?.result;
|
|
190
|
+
if (!isRecord2(result)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const { entity, job } = requestOf(request);
|
|
194
|
+
const found = caseOf(result);
|
|
195
|
+
const key = found === void 0 ? entity : found?.id;
|
|
196
|
+
const decided = decidedOf(result);
|
|
197
|
+
if (decided === null || found === null || key === void 0 || key.length === 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const escalation = escalationOf(result);
|
|
201
|
+
return {
|
|
202
|
+
entity: key,
|
|
203
|
+
...found !== void 0 && { case: found },
|
|
204
|
+
...job !== void 0 && { job },
|
|
205
|
+
...isId(result.releaseId) && { releaseId: result.releaseId },
|
|
206
|
+
...escalation !== void 0 && { escalation },
|
|
207
|
+
...decided,
|
|
208
|
+
context: result.context ?? null,
|
|
209
|
+
evidenceFreshUntil: freshnessOf(result.explanation)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function trim(map, limit) {
|
|
213
|
+
for (const key of map.keys()) {
|
|
214
|
+
if (map.size <= limit) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
map.delete(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function createDecisionRecorder(limits = {}) {
|
|
221
|
+
const maxSessions = limits.sessions ?? 1e3;
|
|
222
|
+
const maxDecisions = limits.decisionsPerSession ?? 200;
|
|
223
|
+
const maxPending = limits.pendingRequests ?? 1e3;
|
|
224
|
+
const requested = /* @__PURE__ */ new Map();
|
|
225
|
+
const decided = /* @__PURE__ */ new Map();
|
|
226
|
+
const checked = /* @__PURE__ */ new Map();
|
|
227
|
+
function record(sessionId, request, output) {
|
|
228
|
+
const decision = decisionFromResult(request, output);
|
|
229
|
+
if (decision === null) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const rows = [...decided.get(sessionId) ?? [], decision].slice(-maxDecisions);
|
|
233
|
+
decided.delete(sessionId);
|
|
234
|
+
decided.set(sessionId, rows);
|
|
235
|
+
trim(decided, maxSessions);
|
|
236
|
+
return decision;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
record,
|
|
240
|
+
noteRequest(callId, request) {
|
|
241
|
+
requested.set(callId, request);
|
|
242
|
+
trim(requested, maxPending);
|
|
243
|
+
},
|
|
244
|
+
noteResult(sessionId, callId, output) {
|
|
245
|
+
const noted = requested.get(callId);
|
|
246
|
+
requested.delete(callId);
|
|
247
|
+
return noted === void 0 ? null : record(sessionId, noted, output);
|
|
248
|
+
},
|
|
249
|
+
decisionsIn: (sessionId) => decided.get(sessionId) ?? [],
|
|
250
|
+
recordOutputCheck(sessionId, jobId, output) {
|
|
251
|
+
const check = outputCheckFromResult(jobId, output);
|
|
252
|
+
if (check === null) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const rows = [...checked.get(sessionId) ?? [], check].slice(-maxDecisions);
|
|
256
|
+
checked.delete(sessionId);
|
|
257
|
+
checked.set(sessionId, rows);
|
|
258
|
+
trim(checked, maxSessions);
|
|
259
|
+
return check;
|
|
260
|
+
},
|
|
261
|
+
outputChecksIn: (sessionId) => checked.get(sessionId) ?? [],
|
|
262
|
+
forget(sessionId) {
|
|
263
|
+
decided.delete(sessionId);
|
|
264
|
+
checked.delete(sessionId);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/draft-action-gate-describe.ts
|
|
270
|
+
function felsparJobDescriber(configuration) {
|
|
271
|
+
let client;
|
|
272
|
+
return async (releaseId, jobId) => {
|
|
273
|
+
client ??= createHostedFelsparClient(configuration);
|
|
274
|
+
return await client.describeJob(releaseId, jobId);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function describedFor(describe, releaseId, jobId) {
|
|
278
|
+
const description = await describe(releaseId, jobId);
|
|
279
|
+
if (description.job !== jobId || description.release.releaseId !== releaseId) {
|
|
280
|
+
throw new Error("Felspar described a different release or job");
|
|
281
|
+
}
|
|
282
|
+
return description;
|
|
283
|
+
}
|
|
284
|
+
function outcomeIn(description, outcome) {
|
|
285
|
+
const matches = description.outcomes.filter((row) => row.id === outcome);
|
|
286
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
287
|
+
}
|
|
288
|
+
async function writesProblem(describe, decided) {
|
|
289
|
+
if (describe === void 0) {
|
|
290
|
+
return "no job describer is configured";
|
|
291
|
+
}
|
|
292
|
+
if (decided.releaseId === void 0) {
|
|
293
|
+
return "Felspar's replay names no release";
|
|
294
|
+
}
|
|
295
|
+
let description;
|
|
296
|
+
try {
|
|
297
|
+
description = await describedFor(describe, decided.releaseId, decided.jobId);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return `Felspar's job description could not be read: ${String(error)}`;
|
|
300
|
+
}
|
|
301
|
+
const outputs = outcomeIn(description, decided.outcome)?.outputs ?? [];
|
|
302
|
+
return outputs.some((row) => row.id === decided.output) ? null : `${decided.outcome} doesn't ask for ${decided.output}`;
|
|
303
|
+
}
|
|
304
|
+
async function subjectOf(describe, releaseId, jobId) {
|
|
305
|
+
const { subject, identity } = await describedFor(describe, releaseId, jobId);
|
|
306
|
+
return subject === void 0 ? null : { type: subject.type, match: subject.match, mode: identity?.mode ?? "provided" };
|
|
307
|
+
}
|
|
308
|
+
var fixOutputCheck = "Fix what check_output flagged, check it again, then retry.";
|
|
309
|
+
var quotedText = (quoted) => typeof quoted?.text === "string" && quoted.text.length > 0 ? quoted.text : null;
|
|
310
|
+
function escalationIdOf(step, description) {
|
|
311
|
+
if (step.kind === "unresolved") {
|
|
312
|
+
const id2 = description?.escalations?.unresolved ?? null;
|
|
313
|
+
return id2 === null ? null : { id: id2, trigger: "unresolved" };
|
|
314
|
+
}
|
|
315
|
+
if (step.replayed !== void 0) {
|
|
316
|
+
return step.replayed.trigger === "outcome" ? { id: step.replayed.id, trigger: "outcome" } : null;
|
|
317
|
+
}
|
|
318
|
+
const id = description === null ? void 0 : outcomeIn(description, step.outcome)?.escalate;
|
|
319
|
+
return id === void 0 ? null : { id, trigger: "outcome" };
|
|
320
|
+
}
|
|
321
|
+
function escalationOf2(step, description) {
|
|
322
|
+
const named = escalationIdOf(step, description);
|
|
323
|
+
if (named === null) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const route = description?.escalations?.routes.find((row) => row.id === named.id);
|
|
327
|
+
const recorded = step.kind === "outcome" && step.recorded?.id === named.id ? step.recorded.why : null;
|
|
328
|
+
const why = quotedText(route?.quote) ?? (recorded === "" ? null : recorded);
|
|
329
|
+
return { ...named, why };
|
|
330
|
+
}
|
|
331
|
+
function nextStepOf(step, description) {
|
|
332
|
+
return step.kind === "unresolved" ? description.whenUnresolved : outcomeIn(description, step.outcome)?.nextStep;
|
|
333
|
+
}
|
|
334
|
+
async function describedStep(step, describe, jobId) {
|
|
335
|
+
if (describe === void 0 || step.releaseId === void 0) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
return await describedFor(describe, step.releaseId, jobId);
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function escalationFor(step, describe, jobId) {
|
|
345
|
+
if (step?.kind === "output-check") {
|
|
346
|
+
return { escalate: fixOutputCheck };
|
|
347
|
+
}
|
|
348
|
+
if (step === void 0) {
|
|
349
|
+
return {};
|
|
350
|
+
}
|
|
351
|
+
const description = await describedStep(step, describe, jobId);
|
|
352
|
+
const escalate = description === null ? null : quotedText(nextStepOf(step, description));
|
|
353
|
+
const escalation = escalationOf2(step, description);
|
|
354
|
+
return {
|
|
355
|
+
...escalate !== null && { escalate },
|
|
356
|
+
...escalation !== null && { escalation }
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/draft-action-gate-cases.ts
|
|
361
|
+
var emailRecipientFields = ["to", "cc", "bcc", "replyTo", "reply_to"];
|
|
362
|
+
var emailSubjectFields = ["subject"];
|
|
363
|
+
var foldCase = (text2) => text2.replaceAll(/[A-Z]/gu, (letter) => letter.toLowerCase());
|
|
364
|
+
var sameField = (names) => {
|
|
365
|
+
const lower = new Set(names.map((name) => name.toLowerCase()));
|
|
366
|
+
return (field) => lower.has(field.toLowerCase());
|
|
367
|
+
};
|
|
368
|
+
function fieldValues(input, names) {
|
|
369
|
+
const isWanted = sameField(names);
|
|
370
|
+
const values = [];
|
|
371
|
+
const unreadable = [];
|
|
372
|
+
for (const [field, value] of Object.entries(input)) {
|
|
373
|
+
if (!isWanted(field)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
for (const item of [value].flat()) {
|
|
377
|
+
if (typeof item === "string") {
|
|
378
|
+
values.push(item.trim());
|
|
379
|
+
} else if (item !== void 0 && item !== null) {
|
|
380
|
+
unreadable.push(field);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { values, unreadable };
|
|
385
|
+
}
|
|
386
|
+
var isAscii = (text2) => /^[!-~]+$/u.test(text2);
|
|
387
|
+
var plainAddress = /^[a-z0-9._+-]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/u;
|
|
388
|
+
var isUnder = (domain, entity) => domain === entity || domain.endsWith(`.${entity}`);
|
|
389
|
+
function domainsIn(text2, entities) {
|
|
390
|
+
return entities.filter((entity) => {
|
|
391
|
+
const escaped = entity.replaceAll(".", String.raw`\.`);
|
|
392
|
+
const whole = new RegExp(`(?:^|[^a-z0-9.-])${escaped}(?:$|[^a-z0-9-])`, "u");
|
|
393
|
+
return whole.test(text2);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function offTestList(recipients, rules) {
|
|
397
|
+
const listed = new Set((rules.testRecipients ?? []).map((address) => address.trim().toLowerCase()));
|
|
398
|
+
const outside = recipients.filter((address) => !listed.has(address));
|
|
399
|
+
return outside.length > 0 ? `recipient not on the test list: ${outside.join(", ")}` : null;
|
|
400
|
+
}
|
|
401
|
+
function testCompany(recipients, subject, scope) {
|
|
402
|
+
const outside = offTestList(recipients, scope.rules);
|
|
403
|
+
if (outside !== null) {
|
|
404
|
+
return { reason: outside };
|
|
405
|
+
}
|
|
406
|
+
const named = domainsIn(subject.toLowerCase(), scope.entities);
|
|
407
|
+
return named.length === 1 ? { company: named[0] } : { reason: `the subject names ${named.length} decided companies, not one` };
|
|
408
|
+
}
|
|
409
|
+
function recipientCompany(recipients, entities) {
|
|
410
|
+
const companies = /* @__PURE__ */ new Set();
|
|
411
|
+
for (const address of recipients) {
|
|
412
|
+
const domain = plainAddress.exec(address)?.[1] ?? "";
|
|
413
|
+
const owners = entities.filter((entity) => isUnder(domain, entity));
|
|
414
|
+
if (owners.length === 0) {
|
|
415
|
+
return { reason: `${address} is not at a company Felspar decided on`, missing: true };
|
|
416
|
+
}
|
|
417
|
+
for (const owner of owners) {
|
|
418
|
+
companies.add(owner);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return companies.size === 1 ? { company: [...companies][0] } : { reason: "the recipients belong to more than one company" };
|
|
422
|
+
}
|
|
423
|
+
function recipientsOf(scope, input) {
|
|
424
|
+
const { rules, rule } = scope;
|
|
425
|
+
const recipientFields = rule.recipientFields ?? rules.recipientFields ?? emailRecipientFields;
|
|
426
|
+
const recipients = fieldValues(input, recipientFields);
|
|
427
|
+
if (recipients.unreadable.length > 0) {
|
|
428
|
+
return { reason: `${recipients.unreadable.join(", ")} holds a value that is not an address` };
|
|
429
|
+
}
|
|
430
|
+
const malformed = recipients.values.find((address) => !isAscii(address) || !plainAddress.test(address.toLowerCase()));
|
|
431
|
+
return malformed === void 0 ? { addresses: recipients.values.map((address) => address.toLowerCase()) } : { reason: `"${malformed}" is not one plain address` };
|
|
432
|
+
}
|
|
433
|
+
function companyFor(scope, input) {
|
|
434
|
+
const { rules, rule } = scope;
|
|
435
|
+
const { addresses, reason } = recipientsOf(scope, input);
|
|
436
|
+
if (addresses === void 0) {
|
|
437
|
+
return { reason };
|
|
438
|
+
}
|
|
439
|
+
if (addresses.length === 0) {
|
|
440
|
+
return { reason: "the call has no recipients" };
|
|
441
|
+
}
|
|
442
|
+
if (rules.testRecipients === void 0) {
|
|
443
|
+
return recipientCompany(addresses, scope.entities);
|
|
444
|
+
}
|
|
445
|
+
const subjectFields = rule.subjectFields ?? rules.subjectFields ?? emailSubjectFields;
|
|
446
|
+
const subject = fieldValues(input, subjectFields).values.join("\n");
|
|
447
|
+
return testCompany(addresses, subject, scope);
|
|
448
|
+
}
|
|
449
|
+
function testListProblem(scope, input) {
|
|
450
|
+
if (scope.rules.testRecipients === void 0) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const { addresses, reason } = recipientsOf({ ...scope, entities: [] }, input);
|
|
454
|
+
return addresses === void 0 ? reason : offTestList(addresses, scope.rules);
|
|
455
|
+
}
|
|
456
|
+
function fieldEntity(scope, field, input) {
|
|
457
|
+
const { values, unreadable } = fieldValues(input, [field]);
|
|
458
|
+
if (unreadable.length > 0 || values.length !== 1) {
|
|
459
|
+
return { reason: `the call must set ${field} to exactly one ID` };
|
|
460
|
+
}
|
|
461
|
+
const [value = ""] = values;
|
|
462
|
+
if (value.length === 0) {
|
|
463
|
+
return { reason: `the call's ${field} is empty` };
|
|
464
|
+
}
|
|
465
|
+
const outside = testListProblem(scope, input);
|
|
466
|
+
return outside === null ? { company: value } : { reason: outside };
|
|
467
|
+
}
|
|
468
|
+
function caseFor(scope, input) {
|
|
469
|
+
const { entity } = scope.rule;
|
|
470
|
+
return entity === void 0 ? companyFor(scope, input) : fieldEntity(scope, entity.field, input);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/draft-action-gate-rules.ts
|
|
474
|
+
var isText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
475
|
+
function ruleProblem(rule) {
|
|
476
|
+
if (rule.outcomes === void 0 === (rule.writes === void 0)) {
|
|
477
|
+
return "this tool's rule must give exactly one of outcomes or writes";
|
|
478
|
+
}
|
|
479
|
+
const { outcomes, writes, entity } = rule;
|
|
480
|
+
if (outcomes !== void 0 && !(Array.isArray(outcomes) && outcomes.every(isText))) {
|
|
481
|
+
return "this tool's rule has unreadable outcomes";
|
|
482
|
+
}
|
|
483
|
+
if (writes !== void 0 && !isText(writes)) {
|
|
484
|
+
return "this tool's rule has an unreadable writes";
|
|
485
|
+
}
|
|
486
|
+
if (entity !== void 0 && !isText(entity.field)) {
|
|
487
|
+
return "this tool's rule has an unreadable entity field";
|
|
488
|
+
}
|
|
489
|
+
const checked = rule.outputCheck?.output;
|
|
490
|
+
return writes !== void 0 && checked !== void 0 && checked !== writes ? "this tool's output check is for a different output than it writes" : null;
|
|
491
|
+
}
|
|
492
|
+
function fieldProblem(refuse, rule, input) {
|
|
493
|
+
const isAllowed = sameField(rule.fields);
|
|
494
|
+
const isRefused = sameField(refuse);
|
|
495
|
+
const unknown = Object.keys(input).filter((field) => !isAllowed(field));
|
|
496
|
+
if (unknown.length > 0) {
|
|
497
|
+
return `the call sets ${unknown.join(", ")}, which this tool's rule doesn't allow`;
|
|
498
|
+
}
|
|
499
|
+
const refused = Object.keys(input).filter((field) => isRefused(field));
|
|
500
|
+
return refused.length > 0 ? `the call sets ${refused.join(", ")}` : null;
|
|
501
|
+
}
|
|
502
|
+
function checkRuleOf(rule) {
|
|
503
|
+
const { outputCheck, writes } = rule;
|
|
504
|
+
return outputCheck !== void 0 && writes !== void 0 && outputCheck.output === void 0 ? { ...outputCheck, output: writes } : outputCheck;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/draft-action-gate-domains.ts
|
|
508
|
+
var publicSuffixes = [
|
|
509
|
+
"ac.uk",
|
|
510
|
+
"co.uk",
|
|
511
|
+
"gov.uk",
|
|
512
|
+
"ltd.uk",
|
|
513
|
+
"me.uk",
|
|
514
|
+
"net.uk",
|
|
515
|
+
"org.uk",
|
|
516
|
+
"plc.uk",
|
|
517
|
+
"sch.uk",
|
|
518
|
+
"com.au",
|
|
519
|
+
"edu.au",
|
|
520
|
+
"gov.au",
|
|
521
|
+
"net.au",
|
|
522
|
+
"org.au",
|
|
523
|
+
"co.nz",
|
|
524
|
+
"net.nz",
|
|
525
|
+
"org.nz",
|
|
526
|
+
"ac.jp",
|
|
527
|
+
"co.jp",
|
|
528
|
+
"ne.jp",
|
|
529
|
+
"or.jp",
|
|
530
|
+
"co.kr",
|
|
531
|
+
"or.kr",
|
|
532
|
+
"co.in",
|
|
533
|
+
"net.in",
|
|
534
|
+
"org.in",
|
|
535
|
+
"co.za",
|
|
536
|
+
"org.za",
|
|
537
|
+
"co.il",
|
|
538
|
+
"co.id",
|
|
539
|
+
"co.ke",
|
|
540
|
+
"co.th",
|
|
541
|
+
"com.ar",
|
|
542
|
+
"com.br",
|
|
543
|
+
"com.cn",
|
|
544
|
+
"com.co",
|
|
545
|
+
"com.eg",
|
|
546
|
+
"com.hk",
|
|
547
|
+
"com.mx",
|
|
548
|
+
"com.my",
|
|
549
|
+
"com.ng",
|
|
550
|
+
"com.ph",
|
|
551
|
+
"com.pk",
|
|
552
|
+
"com.sa",
|
|
553
|
+
"com.sg",
|
|
554
|
+
"com.tr",
|
|
555
|
+
"com.tw",
|
|
556
|
+
"com.ua",
|
|
557
|
+
"com.vn",
|
|
558
|
+
"net.cn",
|
|
559
|
+
"org.cn",
|
|
560
|
+
"gov.cn",
|
|
561
|
+
"edu.cn",
|
|
562
|
+
"appspot.com",
|
|
563
|
+
"azurewebsites.net",
|
|
564
|
+
"blogspot.com",
|
|
565
|
+
"cloudfront.net",
|
|
566
|
+
"firebaseapp.com",
|
|
567
|
+
"github.io",
|
|
568
|
+
"herokuapp.com",
|
|
569
|
+
"netlify.app",
|
|
570
|
+
"pages.dev",
|
|
571
|
+
"vercel.app",
|
|
572
|
+
"web.app",
|
|
573
|
+
"workers.dev"
|
|
574
|
+
];
|
|
575
|
+
var freeMail = [
|
|
576
|
+
"126.com",
|
|
577
|
+
"163.com",
|
|
578
|
+
"aol.com",
|
|
579
|
+
"fastmail.com",
|
|
580
|
+
"gmail.com",
|
|
581
|
+
"googlemail.com",
|
|
582
|
+
"gmx.com",
|
|
583
|
+
"gmx.de",
|
|
584
|
+
"gmx.net",
|
|
585
|
+
"hey.com",
|
|
586
|
+
"hotmail.co.uk",
|
|
587
|
+
"hotmail.com",
|
|
588
|
+
"icloud.com",
|
|
589
|
+
"live.com",
|
|
590
|
+
"mac.com",
|
|
591
|
+
"mail.com",
|
|
592
|
+
"mail.ru",
|
|
593
|
+
"me.com",
|
|
594
|
+
"msn.com",
|
|
595
|
+
"outlook.com",
|
|
596
|
+
"pm.me",
|
|
597
|
+
"proton.me",
|
|
598
|
+
"protonmail.com",
|
|
599
|
+
"qq.com",
|
|
600
|
+
"tuta.io",
|
|
601
|
+
"tutanota.com",
|
|
602
|
+
"web.de",
|
|
603
|
+
"yahoo.co.uk",
|
|
604
|
+
"yahoo.com",
|
|
605
|
+
"yandex.com",
|
|
606
|
+
"yandex.ru",
|
|
607
|
+
"ymail.com",
|
|
608
|
+
"zoho.com"
|
|
609
|
+
];
|
|
610
|
+
var sharedDomains = [...publicSuffixes, ...freeMail];
|
|
611
|
+
var builtIn = new Set(sharedDomains);
|
|
612
|
+
var domainShape = /^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/u;
|
|
613
|
+
function isCompanyDomain(entity, extraShared = []) {
|
|
614
|
+
return domainShape.test(entity) && !builtIn.has(entity) && extraShared.every((domain) => domain.toLowerCase() !== entity);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/draft-action-gate-subject.ts
|
|
618
|
+
var trimmed = (id) => id.trim();
|
|
619
|
+
var folded = (id) => foldCase(id.trim());
|
|
620
|
+
function keyingFor(subject, path) {
|
|
621
|
+
const isFolded = path === "recipients" || subject?.match === "case-insensitive";
|
|
622
|
+
const key = isFolded ? folded : trimmed;
|
|
623
|
+
return { key, decisionKey: (decision) => key(decision.case?.id ?? decision.entity) };
|
|
624
|
+
}
|
|
625
|
+
async function subjectFor(scope, decision) {
|
|
626
|
+
if (decision === void 0) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
const { describe } = scope;
|
|
630
|
+
const { releaseId } = decision;
|
|
631
|
+
const described = describe === void 0 || releaseId === void 0 ? null : await subjectOf(describe, releaseId, scope.rules.job);
|
|
632
|
+
const mode = decision.case?.mode ?? "provided";
|
|
633
|
+
return described === null && mode !== "provided" ? { type: "case", match: "exact", mode } : described;
|
|
634
|
+
}
|
|
635
|
+
var isRecipientJob = (subject) => subject === null || subject.mode === "provided" && subject.match === "case-insensitive";
|
|
636
|
+
var distinct = (keys) => [...new Set(keys)];
|
|
637
|
+
async function pick(scope, found) {
|
|
638
|
+
const { company, keying, subject } = found;
|
|
639
|
+
const decision = scope.decisions.filter((row) => keying.decisionKey(row) === company).at(-1);
|
|
640
|
+
if (decision === void 0) {
|
|
641
|
+
return { reason: `no decision for ${company}`, missing: true };
|
|
642
|
+
}
|
|
643
|
+
const own = await subjectFor(scope, decision);
|
|
644
|
+
return JSON.stringify(own) === JSON.stringify(subject) ? { company, decision, keying } : { reason: `${company} was decided on a release that identifies cases differently` };
|
|
645
|
+
}
|
|
646
|
+
async function onlyCase(scope, subject, input) {
|
|
647
|
+
const keying = keyingFor(subject, "case");
|
|
648
|
+
const keys = distinct(scope.decisions.map((row) => keying.decisionKey(row)));
|
|
649
|
+
if (keys.length !== 1) {
|
|
650
|
+
return {
|
|
651
|
+
reason: keys.length === 0 ? `no decision for this job's ${subject.type}` : `this session decided ${keys.length} ${subject.type} cases and the call doesn't say which; give the tool's rule an entity field holding the case ID`,
|
|
652
|
+
...keys.length === 0 && { missing: true }
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const outside = testListProblem(scope, input);
|
|
656
|
+
const [company = ""] = keys;
|
|
657
|
+
return outside === null ? await pick(scope, { company, keying, subject }) : { reason: outside };
|
|
658
|
+
}
|
|
659
|
+
async function selected(scope, input) {
|
|
660
|
+
const { rules, rule } = scope;
|
|
661
|
+
const subject = await subjectFor(scope, scope.decisions.at(-1));
|
|
662
|
+
if (rule.entity !== void 0) {
|
|
663
|
+
const keying2 = keyingFor(subject, "case");
|
|
664
|
+
const found2 = caseFor({ rules, rule, entities: [] }, input);
|
|
665
|
+
return found2.company === void 0 ? { reason: found2.reason ?? "no case" } : await pick(scope, { company: keying2.key(found2.company), keying: keying2, subject });
|
|
666
|
+
}
|
|
667
|
+
const keying = keyingFor(subject, "recipients");
|
|
668
|
+
const keys = distinct(scope.decisions.map((row) => keying.decisionKey(row)));
|
|
669
|
+
if (subject !== null && !isRecipientJob(subject)) {
|
|
670
|
+
return await onlyCase(scope, subject, input);
|
|
671
|
+
}
|
|
672
|
+
const entities = keys.filter((key) => isCompanyDomain(key, rules.sharedDomains));
|
|
673
|
+
const found = caseFor({ rules, rule, entities }, input);
|
|
674
|
+
return found.company === void 0 ? { reason: found.reason ?? "no case", ...found.missing === true && { missing: true } } : await pick(scope, { company: found.company, keying, subject });
|
|
675
|
+
}
|
|
676
|
+
async function selectCase(scope, input) {
|
|
677
|
+
const decisions = scope.decisions.filter((row) => row.job === void 0 || row.job === scope.rules.job);
|
|
678
|
+
try {
|
|
679
|
+
return await selected({ ...scope, decisions }, input);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
return { reason: `Felspar's job description could not be read: ${String(error)}` };
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/draft-action-gate-verifier.ts
|
|
686
|
+
function felsparVerifier(configuration) {
|
|
687
|
+
let client;
|
|
688
|
+
return async (decision) => {
|
|
689
|
+
client ??= createHostedFelsparClient(configuration);
|
|
690
|
+
const verification = await client.verifyReceipt(decision.executionObjectId, {
|
|
691
|
+
receipt: decision.receipt,
|
|
692
|
+
expectedReceiptDigest: decision.receipt.receiptDigest,
|
|
693
|
+
deliveredCapsule: decision.context ?? null
|
|
694
|
+
});
|
|
695
|
+
return verification.status === "reproduced" && verification.decided !== void 0 ? verification.decided : null;
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/draft-action-gate.ts
|
|
700
|
+
function denialText(verdict) {
|
|
701
|
+
const parts = [verdict.reason];
|
|
702
|
+
if (verdict.escalate !== void 0) {
|
|
703
|
+
parts.push(`Next step: ${verdict.escalate}`);
|
|
704
|
+
}
|
|
705
|
+
const { escalation } = verdict;
|
|
706
|
+
if (escalation !== void 0) {
|
|
707
|
+
const why = escalation.why === null ? "" : `: ${escalation.why}`;
|
|
708
|
+
parts.push(`Escalation ${escalation.id} (${escalation.trigger})${why}`);
|
|
709
|
+
}
|
|
710
|
+
return parts.join(". ");
|
|
711
|
+
}
|
|
712
|
+
function gatedRule(rules, toolName) {
|
|
713
|
+
return Object.hasOwn(rules.tools, toolName) ? rules.tools[toolName] : void 0;
|
|
714
|
+
}
|
|
715
|
+
var isInput = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
716
|
+
function caseProblem(decision, verified) {
|
|
717
|
+
const recorded = decision.case;
|
|
718
|
+
const replayed = verified.case;
|
|
719
|
+
if (recorded === void 0 && replayed === void 0) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const isSame = recorded?.id === replayed?.id && recorded?.mode === replayed?.mode && replayed?.id === verified.entityId;
|
|
723
|
+
return isSame ? null : `the receipt is for case ${replayed?.id ?? "(none)"}, not ${recorded?.id ?? "(none)"}`;
|
|
724
|
+
}
|
|
725
|
+
function replayProblem(rules, found, verified) {
|
|
726
|
+
const { company, decision, keying } = found;
|
|
727
|
+
if (verified.jobId !== rules.job) {
|
|
728
|
+
return `the receipt is for job ${verified.jobId}, not ${rules.job}`;
|
|
729
|
+
}
|
|
730
|
+
if (keying.key(verified.entityId) !== company) {
|
|
731
|
+
return `the receipt is for ${verified.entityId}, not ${company}`;
|
|
732
|
+
}
|
|
733
|
+
const mismatch = caseProblem(decision, verified);
|
|
734
|
+
if (mismatch !== null) {
|
|
735
|
+
return mismatch;
|
|
736
|
+
}
|
|
737
|
+
const { releases } = rules;
|
|
738
|
+
const isUnpinned = releases !== void 0 && !releases.includes(verified.releaseId ?? "");
|
|
739
|
+
return isUnpinned ? `the receipt is for release ${verified.releaseId ?? "(none)"}, which is not allowed` : null;
|
|
740
|
+
}
|
|
741
|
+
function staleness(decision, now) {
|
|
742
|
+
const until = decision.evidenceFreshUntil;
|
|
743
|
+
if (typeof until !== "string") {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
return Date.parse(until) > now ? null : `the decision's evidence went stale at ${until}`;
|
|
747
|
+
}
|
|
748
|
+
async function permitProblem(rule, decided, request) {
|
|
749
|
+
const { verified, decision } = decided;
|
|
750
|
+
const { releaseId, outcome } = verified;
|
|
751
|
+
let problem = null;
|
|
752
|
+
if (rule.writes !== void 0) {
|
|
753
|
+
problem = await writesProblem(request.describe, {
|
|
754
|
+
releaseId,
|
|
755
|
+
jobId: request.rules.job,
|
|
756
|
+
outcome,
|
|
757
|
+
output: rule.writes
|
|
758
|
+
});
|
|
759
|
+
} else if (rule.outcomes?.includes(outcome) !== true) {
|
|
760
|
+
problem = `${verified.entityId} is ${outcome}, which doesn't allow this`;
|
|
761
|
+
}
|
|
762
|
+
if (problem === null) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
const recorded = decision.escalation;
|
|
766
|
+
const step = {
|
|
767
|
+
kind: "outcome",
|
|
768
|
+
releaseId,
|
|
769
|
+
outcome,
|
|
770
|
+
...verified.escalation !== void 0 && { replayed: verified.escalation },
|
|
771
|
+
...recorded?.trigger === "outcome" && { recorded }
|
|
772
|
+
};
|
|
773
|
+
return { allow: false, reason: problem, step };
|
|
774
|
+
}
|
|
775
|
+
async function verifiedFor(rule, found, request) {
|
|
776
|
+
const { company, decision } = found;
|
|
777
|
+
const stale = staleness(decision, (request.now ?? Date.now)());
|
|
778
|
+
if (stale !== null) {
|
|
779
|
+
return { allow: false, reason: stale };
|
|
780
|
+
}
|
|
781
|
+
const verified = await request.verify(decision);
|
|
782
|
+
if (verified === null) {
|
|
783
|
+
return { allow: false, reason: `Felspar did not reproduce ${company}'s receipt` };
|
|
784
|
+
}
|
|
785
|
+
const problem = replayProblem(request.rules, found, verified);
|
|
786
|
+
if (problem !== null) {
|
|
787
|
+
return { allow: false, reason: problem };
|
|
788
|
+
}
|
|
789
|
+
const { outcome } = verified;
|
|
790
|
+
if (outcome === null) {
|
|
791
|
+
const reason = `${verified.entityId} is undecided, which doesn't allow this`;
|
|
792
|
+
return { allow: false, reason, step: { kind: "unresolved", releaseId: verified.releaseId } };
|
|
793
|
+
}
|
|
794
|
+
const denied = await permitProblem(rule, { verified: { ...verified, outcome }, decision }, request);
|
|
795
|
+
if (denied !== null) {
|
|
796
|
+
return denied;
|
|
797
|
+
}
|
|
798
|
+
const checkRule = checkRuleOf(rule);
|
|
799
|
+
if (checkRule !== void 0) {
|
|
800
|
+
const checked = await outputCheckProblem(checkRule, {
|
|
801
|
+
input: found.input,
|
|
802
|
+
decisionReceiptDigest: decision.receipt.receiptDigest,
|
|
803
|
+
checks: request.outputChecks ?? [],
|
|
804
|
+
verify: request.verifyOutputCheck
|
|
805
|
+
});
|
|
806
|
+
if (checked !== null) {
|
|
807
|
+
return { allow: false, reason: checked, step: { kind: "output-check" } };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
allow: true,
|
|
812
|
+
reason: `${company} is ${outcome}; Felspar reproduced the receipt`,
|
|
813
|
+
decision: { ...decision, outcome }
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async function decide(request) {
|
|
817
|
+
const { rules, toolName, decisions } = request;
|
|
818
|
+
const rule = gatedRule(rules, toolName);
|
|
819
|
+
if (rule === void 0) {
|
|
820
|
+
return { allow: false, reason: "not a gated tool" };
|
|
821
|
+
}
|
|
822
|
+
const invalid = ruleProblem(rule);
|
|
823
|
+
if (invalid !== null) {
|
|
824
|
+
return { allow: false, reason: invalid };
|
|
825
|
+
}
|
|
826
|
+
const input = isInput(request.input) ? request.input : {};
|
|
827
|
+
const problem = fieldProblem(rules.refuse ?? [], rule, input);
|
|
828
|
+
if (problem !== null) {
|
|
829
|
+
return { allow: false, reason: problem };
|
|
830
|
+
}
|
|
831
|
+
const selected2 = await selectCase({ rules, rule, decisions, describe: request.describe }, input);
|
|
832
|
+
if (selected2.company === void 0) {
|
|
833
|
+
const pinned = rules.releases?.length === 1 ? rules.releases[0] : void 0;
|
|
834
|
+
return {
|
|
835
|
+
allow: false,
|
|
836
|
+
reason: selected2.reason,
|
|
837
|
+
...selected2.missing === true && { step: { kind: "unresolved", releaseId: pinned } }
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return await verifiedFor(rule, { ...selected2, input }, request);
|
|
841
|
+
}
|
|
842
|
+
async function checkAction(request) {
|
|
843
|
+
let decided;
|
|
844
|
+
try {
|
|
845
|
+
decided = await decide(request);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
return { allow: false, reason: `gate error: ${String(error)}` };
|
|
848
|
+
}
|
|
849
|
+
if (decided.allow) {
|
|
850
|
+
return decided;
|
|
851
|
+
}
|
|
852
|
+
const { step, ...denied } = decided;
|
|
853
|
+
return { ...denied, ...await escalationFor(step, request.describe, request.rules.job) };
|
|
854
|
+
}
|
|
855
|
+
function report(onVerdict, toolName, verdict) {
|
|
856
|
+
if (onVerdict === void 0) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
onVerdict(toolName, verdict);
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function createActionGate(config) {
|
|
865
|
+
const { verify, decisions = createDecisionRecorder(), decisionTool, outputCheckTool, verifyOutputCheck, describe, now, onVerdict, ...rules } = config;
|
|
866
|
+
return {
|
|
867
|
+
rules,
|
|
868
|
+
decisions,
|
|
869
|
+
isGated: (toolName) => gatedRule(rules, toolName) !== void 0,
|
|
870
|
+
isDecisionTool: (toolName) => toolName === decisionTool,
|
|
871
|
+
isOutputCheckTool: (toolName) => outputCheckTool !== void 0 && toolName === outputCheckTool,
|
|
872
|
+
async check(sessionId, toolName, input) {
|
|
873
|
+
const verdict = await checkAction({
|
|
874
|
+
rules,
|
|
875
|
+
toolName,
|
|
876
|
+
input,
|
|
877
|
+
decisions: decisions.decisionsIn(sessionId),
|
|
878
|
+
verify,
|
|
879
|
+
outputChecks: decisions.outputChecksIn(sessionId),
|
|
880
|
+
...verifyOutputCheck !== void 0 && { verifyOutputCheck },
|
|
881
|
+
...describe !== void 0 && { describe },
|
|
882
|
+
...now !== void 0 && { now }
|
|
883
|
+
});
|
|
884
|
+
report(onVerdict, toolName, verdict);
|
|
885
|
+
return verdict;
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export {
|
|
891
|
+
outputCheckFromResult,
|
|
892
|
+
outputTextDigest,
|
|
893
|
+
felsparOutputCheckVerifier,
|
|
894
|
+
jobOf,
|
|
895
|
+
entityIdOf,
|
|
896
|
+
decisionFromResult,
|
|
897
|
+
createDecisionRecorder,
|
|
898
|
+
felsparJobDescriber,
|
|
899
|
+
emailRecipientFields,
|
|
900
|
+
emailSubjectFields,
|
|
901
|
+
sharedDomains,
|
|
902
|
+
isCompanyDomain,
|
|
903
|
+
felsparVerifier,
|
|
904
|
+
denialText,
|
|
905
|
+
gatedRule,
|
|
906
|
+
checkAction,
|
|
907
|
+
createActionGate
|
|
908
|
+
};
|