@felspar/sdk 0.1.1 → 0.2.0
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 +99 -27
- package/bin/felspar-sdk.mjs +10 -0
- package/bin/skill.mjs +33 -0
- package/dist/chunks/chunk-247XZ6IY.js +54 -0
- package/dist/chunks/{chunk-C2VMKF3S.js → chunk-HACK7D24.js} +1138 -821
- package/dist/chunks/{chunk-6EZBI7O6.js → chunk-IY6A2423.js} +276 -116
- package/dist/draft-action-gate-claude.js +4 -3
- package/dist/draft-action-gate-eve.js +5 -48
- package/dist/draft-action-gate-openai.js +4 -3
- 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 +30 -0
- package/dist/types/draft-action-gate-describe.d.ts +43 -0
- package/dist/types/draft-action-gate-rules.d.ts +15 -0
- package/dist/types/draft-action-gate.d.ts +14 -10
- package/dist/types/draft-hosted-client.d.ts +278 -2
- package/dist/types/eve.d.ts +22 -0
- package/package.json +10 -1
- package/skills/felspar/SKILL.md +127 -0
|
@@ -1,6 +1,119 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createHostedFelsparClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HACK7D24.js";
|
|
4
|
+
|
|
5
|
+
// src/draft-action-gate-cases.ts
|
|
6
|
+
var emailRecipientFields = ["to", "cc", "bcc", "replyTo", "reply_to"];
|
|
7
|
+
var emailSubjectFields = ["subject"];
|
|
8
|
+
var foldCase = (text2) => text2.replaceAll(/[A-Z]/gu, (letter) => letter.toLowerCase());
|
|
9
|
+
var caseKeyOf = (rule) => rule.entity === void 0 ? foldCase : (id) => id.trim();
|
|
10
|
+
var sameField = (names) => {
|
|
11
|
+
const lower = new Set(names.map((name) => name.toLowerCase()));
|
|
12
|
+
return (field) => lower.has(field.toLowerCase());
|
|
13
|
+
};
|
|
14
|
+
function fieldValues(input, names) {
|
|
15
|
+
const isWanted = sameField(names);
|
|
16
|
+
const values = [];
|
|
17
|
+
const unreadable = [];
|
|
18
|
+
for (const [field, value] of Object.entries(input)) {
|
|
19
|
+
if (!isWanted(field)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
for (const item of [value].flat()) {
|
|
23
|
+
if (typeof item === "string") {
|
|
24
|
+
values.push(item.trim());
|
|
25
|
+
} else if (item !== void 0 && item !== null) {
|
|
26
|
+
unreadable.push(field);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { values, unreadable };
|
|
31
|
+
}
|
|
32
|
+
var isAscii = (text2) => /^[!-~]+$/u.test(text2);
|
|
33
|
+
var plainAddress = /^[a-z0-9._+-]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/u;
|
|
34
|
+
var isUnder = (domain, entity) => domain === entity || domain.endsWith(`.${entity}`);
|
|
35
|
+
function domainsIn(text2, entities) {
|
|
36
|
+
return entities.filter((entity) => {
|
|
37
|
+
const escaped = entity.replaceAll(".", String.raw`\.`);
|
|
38
|
+
const whole = new RegExp(`(?:^|[^a-z0-9.-])${escaped}(?:$|[^a-z0-9-])`, "u");
|
|
39
|
+
return whole.test(text2);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function offTestList(recipients, rules) {
|
|
43
|
+
const listed = new Set((rules.testRecipients ?? []).map((address) => address.trim().toLowerCase()));
|
|
44
|
+
const outside = recipients.filter((address) => !listed.has(address));
|
|
45
|
+
return outside.length > 0 ? `recipient not on the test list: ${outside.join(", ")}` : null;
|
|
46
|
+
}
|
|
47
|
+
function testCompany(recipients, subject, scope) {
|
|
48
|
+
const outside = offTestList(recipients, scope.rules);
|
|
49
|
+
if (outside !== null) {
|
|
50
|
+
return { reason: outside };
|
|
51
|
+
}
|
|
52
|
+
const named = domainsIn(subject.toLowerCase(), scope.entities);
|
|
53
|
+
return named.length === 1 ? { company: named[0] } : { reason: `the subject names ${named.length} decided companies, not one` };
|
|
54
|
+
}
|
|
55
|
+
function recipientCompany(recipients, entities) {
|
|
56
|
+
const companies = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const address of recipients) {
|
|
58
|
+
const domain = plainAddress.exec(address)?.[1] ?? "";
|
|
59
|
+
const owners = entities.filter((entity) => isUnder(domain, entity));
|
|
60
|
+
if (owners.length === 0) {
|
|
61
|
+
return { reason: `${address} is not at a company Felspar decided on`, missing: true };
|
|
62
|
+
}
|
|
63
|
+
for (const owner of owners) {
|
|
64
|
+
companies.add(owner);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return companies.size === 1 ? { company: [...companies][0] } : { reason: "the recipients belong to more than one company" };
|
|
68
|
+
}
|
|
69
|
+
function recipientsOf(scope, input) {
|
|
70
|
+
const { rules, rule } = scope;
|
|
71
|
+
const recipientFields = rule.recipientFields ?? rules.recipientFields ?? emailRecipientFields;
|
|
72
|
+
const recipients = fieldValues(input, recipientFields);
|
|
73
|
+
if (recipients.unreadable.length > 0) {
|
|
74
|
+
return { reason: `${recipients.unreadable.join(", ")} holds a value that is not an address` };
|
|
75
|
+
}
|
|
76
|
+
const malformed = recipients.values.find((address) => !isAscii(address) || !plainAddress.test(address.toLowerCase()));
|
|
77
|
+
return malformed === void 0 ? { addresses: recipients.values.map((address) => address.toLowerCase()) } : { reason: `"${malformed}" is not one plain address` };
|
|
78
|
+
}
|
|
79
|
+
function companyFor(scope, input) {
|
|
80
|
+
const { rules, rule } = scope;
|
|
81
|
+
const { addresses, reason } = recipientsOf(scope, input);
|
|
82
|
+
if (addresses === void 0) {
|
|
83
|
+
return { reason };
|
|
84
|
+
}
|
|
85
|
+
if (addresses.length === 0) {
|
|
86
|
+
return { reason: "the call has no recipients" };
|
|
87
|
+
}
|
|
88
|
+
if (rules.testRecipients === void 0) {
|
|
89
|
+
return recipientCompany(addresses, scope.entities);
|
|
90
|
+
}
|
|
91
|
+
const subjectFields = rule.subjectFields ?? rules.subjectFields ?? emailSubjectFields;
|
|
92
|
+
const subject = fieldValues(input, subjectFields).values.join("\n");
|
|
93
|
+
return testCompany(addresses, subject, scope);
|
|
94
|
+
}
|
|
95
|
+
function fieldEntity(scope, field, input) {
|
|
96
|
+
const { values, unreadable } = fieldValues(input, [field]);
|
|
97
|
+
if (unreadable.length > 0 || values.length !== 1) {
|
|
98
|
+
return { reason: `the call must set ${field} to exactly one ID` };
|
|
99
|
+
}
|
|
100
|
+
const [value = ""] = values;
|
|
101
|
+
if (value.length === 0) {
|
|
102
|
+
return { reason: `the call's ${field} is empty` };
|
|
103
|
+
}
|
|
104
|
+
if (scope.rules.testRecipients !== void 0) {
|
|
105
|
+
const { addresses, reason } = recipientsOf(scope, input);
|
|
106
|
+
const outside = addresses === void 0 ? reason : offTestList(addresses, scope.rules);
|
|
107
|
+
if (outside !== null) {
|
|
108
|
+
return { reason: outside };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { company: value };
|
|
112
|
+
}
|
|
113
|
+
function caseFor(scope, input) {
|
|
114
|
+
const { entity } = scope.rule;
|
|
115
|
+
return entity === void 0 ? companyFor(scope, input) : fieldEntity(scope, entity.field, input);
|
|
116
|
+
}
|
|
4
117
|
|
|
5
118
|
// src/draft-action-gate-outputs.ts
|
|
6
119
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -75,10 +188,10 @@ async function outputCheckProblem(rule, call) {
|
|
|
75
188
|
let check;
|
|
76
189
|
for (const row of call.checks) {
|
|
77
190
|
const isOutput = rule.output === void 0 || row.output === rule.output;
|
|
78
|
-
const
|
|
191
|
+
const isText2 = row.artifactDigest === digest;
|
|
79
192
|
const isDecision = row.decisionReceiptDigest === call.decisionReceiptDigest;
|
|
80
193
|
const isPass = row.status === "pass";
|
|
81
|
-
if (isPass &&
|
|
194
|
+
if (isPass && isText2 && isDecision && isOutput) {
|
|
82
195
|
check = row;
|
|
83
196
|
}
|
|
84
197
|
}
|
|
@@ -231,6 +344,64 @@ function createDecisionRecorder(limits = {}) {
|
|
|
231
344
|
};
|
|
232
345
|
}
|
|
233
346
|
|
|
347
|
+
// src/draft-action-gate-describe.ts
|
|
348
|
+
function felsparJobDescriber(configuration) {
|
|
349
|
+
let client;
|
|
350
|
+
return async (releaseId, jobId) => {
|
|
351
|
+
client ??= createHostedFelsparClient(configuration);
|
|
352
|
+
return await client.describeJob(releaseId, jobId);
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function describedFor(describe, releaseId, jobId) {
|
|
356
|
+
const description = await describe(releaseId, jobId);
|
|
357
|
+
if (description.job !== jobId || description.release.releaseId !== releaseId) {
|
|
358
|
+
throw new Error("Felspar described a different release or job");
|
|
359
|
+
}
|
|
360
|
+
return description;
|
|
361
|
+
}
|
|
362
|
+
function outcomeIn(description, outcome) {
|
|
363
|
+
const matches = description.outcomes.filter((row) => row.id === outcome);
|
|
364
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
365
|
+
}
|
|
366
|
+
async function writesProblem(describe, decided) {
|
|
367
|
+
if (describe === void 0) {
|
|
368
|
+
return "no job describer is configured";
|
|
369
|
+
}
|
|
370
|
+
if (decided.releaseId === void 0) {
|
|
371
|
+
return "Felspar's replay names no release";
|
|
372
|
+
}
|
|
373
|
+
let description;
|
|
374
|
+
try {
|
|
375
|
+
description = await describedFor(describe, decided.releaseId, decided.jobId);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return `Felspar's job description could not be read: ${String(error)}`;
|
|
378
|
+
}
|
|
379
|
+
const outputs = outcomeIn(description, decided.outcome)?.outputs ?? [];
|
|
380
|
+
return outputs.some((row) => row.id === decided.output) ? null : `${decided.outcome} doesn't ask for ${decided.output}`;
|
|
381
|
+
}
|
|
382
|
+
var fixOutputCheck = "Fix what check_output flagged, check it again, then retry.";
|
|
383
|
+
async function quotedStep(step, describe, jobId) {
|
|
384
|
+
if (step.releaseId === void 0) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const description = await describedFor(describe, step.releaseId, jobId);
|
|
388
|
+
const quoted = step.kind === "unresolved" ? description.whenUnresolved : outcomeIn(description, step.outcome)?.nextStep;
|
|
389
|
+
return typeof quoted?.text === "string" && quoted.text.length > 0 ? quoted.text : null;
|
|
390
|
+
}
|
|
391
|
+
async function escalationFor(step, describe, jobId) {
|
|
392
|
+
if (step?.kind === "output-check") {
|
|
393
|
+
return fixOutputCheck;
|
|
394
|
+
}
|
|
395
|
+
if (step === void 0 || describe === void 0) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
return await quotedStep(step, describe, jobId);
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
234
405
|
// src/draft-action-gate-domains.ts
|
|
235
406
|
var publicSuffixes = [
|
|
236
407
|
"ac.uk",
|
|
@@ -341,6 +512,40 @@ function isCompanyDomain(entity, extraShared = []) {
|
|
|
341
512
|
return domainShape.test(entity) && !builtIn.has(entity) && extraShared.every((domain) => domain.toLowerCase() !== entity);
|
|
342
513
|
}
|
|
343
514
|
|
|
515
|
+
// src/draft-action-gate-rules.ts
|
|
516
|
+
var isText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
517
|
+
function ruleProblem(rule) {
|
|
518
|
+
if (rule.outcomes === void 0 === (rule.writes === void 0)) {
|
|
519
|
+
return "this tool's rule must give exactly one of outcomes or writes";
|
|
520
|
+
}
|
|
521
|
+
const { outcomes, writes, entity } = rule;
|
|
522
|
+
if (outcomes !== void 0 && !(Array.isArray(outcomes) && outcomes.every(isText))) {
|
|
523
|
+
return "this tool's rule has unreadable outcomes";
|
|
524
|
+
}
|
|
525
|
+
if (writes !== void 0 && !isText(writes)) {
|
|
526
|
+
return "this tool's rule has an unreadable writes";
|
|
527
|
+
}
|
|
528
|
+
if (entity !== void 0 && !isText(entity.field)) {
|
|
529
|
+
return "this tool's rule has an unreadable entity field";
|
|
530
|
+
}
|
|
531
|
+
const checked = rule.outputCheck?.output;
|
|
532
|
+
return writes !== void 0 && checked !== void 0 && checked !== writes ? "this tool's output check is for a different output than it writes" : null;
|
|
533
|
+
}
|
|
534
|
+
function fieldProblem(refuse, rule, input) {
|
|
535
|
+
const isAllowed = sameField(rule.fields);
|
|
536
|
+
const isRefused = sameField(refuse);
|
|
537
|
+
const unknown = Object.keys(input).filter((field) => !isAllowed(field));
|
|
538
|
+
if (unknown.length > 0) {
|
|
539
|
+
return `the call sets ${unknown.join(", ")}, which this tool's rule doesn't allow`;
|
|
540
|
+
}
|
|
541
|
+
const refused = Object.keys(input).filter((field) => isRefused(field));
|
|
542
|
+
return refused.length > 0 ? `the call sets ${refused.join(", ")}` : null;
|
|
543
|
+
}
|
|
544
|
+
function checkRuleOf(rule) {
|
|
545
|
+
const { outputCheck, writes } = rule;
|
|
546
|
+
return outputCheck !== void 0 && writes !== void 0 && outputCheck.output === void 0 ? { ...outputCheck, output: writes } : outputCheck;
|
|
547
|
+
}
|
|
548
|
+
|
|
344
549
|
// src/draft-action-gate-verifier.ts
|
|
345
550
|
function felsparVerifier(configuration) {
|
|
346
551
|
let client;
|
|
@@ -356,104 +561,19 @@ function felsparVerifier(configuration) {
|
|
|
356
561
|
}
|
|
357
562
|
|
|
358
563
|
// src/draft-action-gate.ts
|
|
359
|
-
|
|
360
|
-
|
|
564
|
+
function denialText(verdict) {
|
|
565
|
+
return verdict.escalate === void 0 ? verdict.reason : `${verdict.reason}. Next step: ${verdict.escalate}`;
|
|
566
|
+
}
|
|
361
567
|
function gatedRule(rules, toolName) {
|
|
362
568
|
return Object.hasOwn(rules.tools, toolName) ? rules.tools[toolName] : void 0;
|
|
363
569
|
}
|
|
364
570
|
var isInput = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
return (field) => lower.has(field.toLowerCase());
|
|
368
|
-
};
|
|
369
|
-
function fieldProblem(rules, rule, input) {
|
|
370
|
-
const isAllowed = sameField(rule.fields);
|
|
371
|
-
const isRefused = sameField(rules.refuse ?? []);
|
|
372
|
-
const unknown = Object.keys(input).filter((field) => !isAllowed(field));
|
|
373
|
-
if (unknown.length > 0) {
|
|
374
|
-
return `the call sets ${unknown.join(", ")}, which this tool's rule doesn't allow`;
|
|
375
|
-
}
|
|
376
|
-
const refused = Object.keys(input).filter((field) => isRefused(field));
|
|
377
|
-
return refused.length > 0 ? `the call sets ${refused.join(", ")}` : null;
|
|
378
|
-
}
|
|
379
|
-
function fieldValues(input, names) {
|
|
380
|
-
const isWanted = sameField(names);
|
|
381
|
-
const values = [];
|
|
382
|
-
const unreadable = [];
|
|
383
|
-
for (const [field, value] of Object.entries(input)) {
|
|
384
|
-
if (!isWanted(field)) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
for (const item of [value].flat()) {
|
|
388
|
-
if (typeof item === "string") {
|
|
389
|
-
values.push(item.trim());
|
|
390
|
-
} else if (item !== void 0 && item !== null) {
|
|
391
|
-
unreadable.push(field);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return { values, unreadable };
|
|
396
|
-
}
|
|
397
|
-
var isAscii = (text2) => /^[!-~]+$/u.test(text2);
|
|
398
|
-
var plainAddress = /^[a-z0-9._+-]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/u;
|
|
399
|
-
var isUnder = (domain, entity) => domain === entity || domain.endsWith(`.${entity}`);
|
|
400
|
-
function domainsIn(text2, entities) {
|
|
401
|
-
return entities.filter((entity) => {
|
|
402
|
-
const escaped = entity.replaceAll(".", String.raw`\.`);
|
|
403
|
-
const whole = new RegExp(`(?:^|[^a-z0-9.-])${escaped}(?:$|[^a-z0-9-])`, "u");
|
|
404
|
-
return whole.test(text2);
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
function testCompany(recipients, subject, scope) {
|
|
408
|
-
const listed = new Set((scope.rules.testRecipients ?? []).map((address) => address.trim().toLowerCase()));
|
|
409
|
-
const outside = recipients.filter((address) => !listed.has(address));
|
|
410
|
-
if (outside.length > 0) {
|
|
411
|
-
return { reason: `recipient not on the test list: ${outside.join(", ")}` };
|
|
412
|
-
}
|
|
413
|
-
const named = domainsIn(subject.toLowerCase(), scope.entities);
|
|
414
|
-
return named.length === 1 ? { company: named[0] } : { reason: `the subject names ${named.length} decided companies, not one` };
|
|
415
|
-
}
|
|
416
|
-
function recipientCompany(recipients, entities) {
|
|
417
|
-
const companies = /* @__PURE__ */ new Set();
|
|
418
|
-
for (const address of recipients) {
|
|
419
|
-
const domain = plainAddress.exec(address)?.[1] ?? "";
|
|
420
|
-
const owners = entities.filter((entity) => isUnder(domain, entity));
|
|
421
|
-
if (owners.length === 0) {
|
|
422
|
-
return { reason: `${address} is not at a company Felspar decided on` };
|
|
423
|
-
}
|
|
424
|
-
for (const owner of owners) {
|
|
425
|
-
companies.add(owner);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return companies.size === 1 ? { company: [...companies][0] } : { reason: "the recipients belong to more than one company" };
|
|
429
|
-
}
|
|
430
|
-
function companyFor(scope, input) {
|
|
431
|
-
const { rules, rule } = scope;
|
|
432
|
-
const recipientFields = rule.recipientFields ?? rules.recipientFields ?? emailRecipientFields;
|
|
433
|
-
const recipients = fieldValues(input, recipientFields);
|
|
434
|
-
if (recipients.unreadable.length > 0) {
|
|
435
|
-
return { reason: `${recipients.unreadable.join(", ")} holds a value that is not an address` };
|
|
436
|
-
}
|
|
437
|
-
if (recipients.values.length === 0) {
|
|
438
|
-
return { reason: "the call has no recipients" };
|
|
439
|
-
}
|
|
440
|
-
const malformed = recipients.values.find((address) => !isAscii(address) || !plainAddress.test(address.toLowerCase()));
|
|
441
|
-
if (malformed !== void 0) {
|
|
442
|
-
return { reason: `"${malformed}" is not one plain address` };
|
|
443
|
-
}
|
|
444
|
-
const addresses = recipients.values.map((address) => address.toLowerCase());
|
|
445
|
-
if (rules.testRecipients === void 0) {
|
|
446
|
-
return recipientCompany(addresses, scope.entities);
|
|
447
|
-
}
|
|
448
|
-
const subjectFields = rule.subjectFields ?? rules.subjectFields ?? emailSubjectFields;
|
|
449
|
-
const subject = fieldValues(input, subjectFields).values.join("\n");
|
|
450
|
-
return testCompany(addresses, subject, scope);
|
|
451
|
-
}
|
|
452
|
-
function replayProblem(rules, company, verified) {
|
|
571
|
+
function replayProblem(scope, company, verified) {
|
|
572
|
+
const { rules } = scope;
|
|
453
573
|
if (verified.jobId !== rules.job) {
|
|
454
574
|
return `the receipt is for job ${verified.jobId}, not ${rules.job}`;
|
|
455
575
|
}
|
|
456
|
-
if (verified.entityId
|
|
576
|
+
if (caseKeyOf(scope.rule)(verified.entityId) !== company) {
|
|
457
577
|
return `the receipt is for ${verified.entityId}, not ${company}`;
|
|
458
578
|
}
|
|
459
579
|
const { releases } = rules;
|
|
@@ -467,8 +587,22 @@ function staleness(decision, now) {
|
|
|
467
587
|
}
|
|
468
588
|
return Date.parse(until) > now ? null : `the decision's evidence went stale at ${until}`;
|
|
469
589
|
}
|
|
470
|
-
async function
|
|
471
|
-
const {
|
|
590
|
+
async function permitProblem(rule, verified, request) {
|
|
591
|
+
const { releaseId, outcome } = verified;
|
|
592
|
+
let problem = null;
|
|
593
|
+
if (rule.writes !== void 0) {
|
|
594
|
+
problem = await writesProblem(request.describe, {
|
|
595
|
+
releaseId,
|
|
596
|
+
jobId: request.rules.job,
|
|
597
|
+
outcome,
|
|
598
|
+
output: rule.writes
|
|
599
|
+
});
|
|
600
|
+
} else if (rule.outcomes?.includes(outcome) !== true) {
|
|
601
|
+
problem = `${verified.entityId} is ${outcome}, which doesn't allow this`;
|
|
602
|
+
}
|
|
603
|
+
return problem === null ? null : { allow: false, reason: problem, step: { kind: "outcome", releaseId, outcome } };
|
|
604
|
+
}
|
|
605
|
+
async function verifiedFor(rule, found, request) {
|
|
472
606
|
const { company, decision } = found;
|
|
473
607
|
const stale = staleness(decision, (request.now ?? Date.now)());
|
|
474
608
|
if (stale !== null) {
|
|
@@ -478,29 +612,35 @@ async function verifiedFor(scope, found, request) {
|
|
|
478
612
|
if (verified === null) {
|
|
479
613
|
return { allow: false, reason: `Felspar did not reproduce ${company}'s receipt` };
|
|
480
614
|
}
|
|
481
|
-
const problem = replayProblem(rules, company, verified);
|
|
615
|
+
const problem = replayProblem({ rules: request.rules, rule }, company, verified);
|
|
482
616
|
if (problem !== null) {
|
|
483
617
|
return { allow: false, reason: problem };
|
|
484
618
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
619
|
+
const { outcome } = verified;
|
|
620
|
+
if (outcome === null) {
|
|
621
|
+
const reason = `${verified.entityId} is undecided, which doesn't allow this`;
|
|
622
|
+
return { allow: false, reason, step: { kind: "unresolved", releaseId: verified.releaseId } };
|
|
488
623
|
}
|
|
489
|
-
|
|
490
|
-
|
|
624
|
+
const denied = await permitProblem(rule, { ...verified, outcome }, request);
|
|
625
|
+
if (denied !== null) {
|
|
626
|
+
return denied;
|
|
627
|
+
}
|
|
628
|
+
const checkRule = checkRuleOf(rule);
|
|
629
|
+
if (checkRule !== void 0) {
|
|
630
|
+
const checked = await outputCheckProblem(checkRule, {
|
|
491
631
|
input: found.input,
|
|
492
632
|
decisionReceiptDigest: decision.receipt.receiptDigest,
|
|
493
633
|
checks: request.outputChecks ?? [],
|
|
494
634
|
verify: request.verifyOutputCheck
|
|
495
635
|
});
|
|
496
636
|
if (checked !== null) {
|
|
497
|
-
return { allow: false, reason: checked };
|
|
637
|
+
return { allow: false, reason: checked, step: { kind: "output-check" } };
|
|
498
638
|
}
|
|
499
639
|
}
|
|
500
640
|
return {
|
|
501
641
|
allow: true,
|
|
502
|
-
reason: `${company} is ${
|
|
503
|
-
decision: { ...decision, outcome
|
|
642
|
+
reason: `${company} is ${outcome}; Felspar reproduced the receipt`,
|
|
643
|
+
decision: { ...decision, outcome }
|
|
504
644
|
};
|
|
505
645
|
}
|
|
506
646
|
async function decide(request) {
|
|
@@ -509,26 +649,43 @@ async function decide(request) {
|
|
|
509
649
|
if (rule === void 0) {
|
|
510
650
|
return { allow: false, reason: "not a gated tool" };
|
|
511
651
|
}
|
|
652
|
+
const invalid = ruleProblem(rule);
|
|
653
|
+
if (invalid !== null) {
|
|
654
|
+
return { allow: false, reason: invalid };
|
|
655
|
+
}
|
|
512
656
|
const input = isInput(request.input) ? request.input : {};
|
|
513
|
-
const problem = fieldProblem(rules, rule, input);
|
|
657
|
+
const problem = fieldProblem(rules.refuse ?? [], rule, input);
|
|
514
658
|
if (problem !== null) {
|
|
515
659
|
return { allow: false, reason: problem };
|
|
516
660
|
}
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
const
|
|
661
|
+
const keyOf = caseKeyOf(rule);
|
|
662
|
+
const entities = rule.entity === void 0 ? [...new Set(decisions.map((row) => keyOf(row.entity)))].filter((entity) => isCompanyDomain(entity, rules.sharedDomains)) : [];
|
|
663
|
+
const pinned = rules.releases?.length === 1 ? rules.releases[0] : void 0;
|
|
664
|
+
const unresolved = { kind: "unresolved", releaseId: pinned };
|
|
665
|
+
const { company, reason, missing } = caseFor({ rules, rule, entities }, input);
|
|
520
666
|
if (company === void 0) {
|
|
521
|
-
return {
|
|
667
|
+
return {
|
|
668
|
+
allow: false,
|
|
669
|
+
reason: reason ?? "no case",
|
|
670
|
+
...missing === true && { step: unresolved }
|
|
671
|
+
};
|
|
522
672
|
}
|
|
523
|
-
const decision = decisions.filter((row) => row.entity
|
|
524
|
-
return decision === void 0 ? { allow: false, reason: `no decision for ${company}
|
|
673
|
+
const decision = decisions.filter((row) => keyOf(row.entity) === company).at(-1);
|
|
674
|
+
return decision === void 0 ? { allow: false, reason: `no decision for ${company}`, step: unresolved } : await verifiedFor(rule, { company, decision, input }, request);
|
|
525
675
|
}
|
|
526
676
|
async function checkAction(request) {
|
|
677
|
+
let decided;
|
|
527
678
|
try {
|
|
528
|
-
|
|
679
|
+
decided = await decide(request);
|
|
529
680
|
} catch (error) {
|
|
530
681
|
return { allow: false, reason: `gate error: ${String(error)}` };
|
|
531
682
|
}
|
|
683
|
+
if (decided.allow) {
|
|
684
|
+
return decided;
|
|
685
|
+
}
|
|
686
|
+
const { step, ...denied } = decided;
|
|
687
|
+
const escalate = await escalationFor(step, request.describe, request.rules.job);
|
|
688
|
+
return escalate === null ? denied : { ...denied, escalate };
|
|
532
689
|
}
|
|
533
690
|
function report(onVerdict, toolName, verdict) {
|
|
534
691
|
if (onVerdict === void 0) {
|
|
@@ -540,7 +697,7 @@ function report(onVerdict, toolName, verdict) {
|
|
|
540
697
|
}
|
|
541
698
|
}
|
|
542
699
|
function createActionGate(config) {
|
|
543
|
-
const { verify, decisions = createDecisionRecorder(), decisionTool, outputCheckTool, verifyOutputCheck, now, onVerdict, ...rules } = config;
|
|
700
|
+
const { verify, decisions = createDecisionRecorder(), decisionTool, outputCheckTool, verifyOutputCheck, describe, now, onVerdict, ...rules } = config;
|
|
544
701
|
return {
|
|
545
702
|
rules,
|
|
546
703
|
decisions,
|
|
@@ -556,6 +713,7 @@ function createActionGate(config) {
|
|
|
556
713
|
verify,
|
|
557
714
|
outputChecks: decisions.outputChecksIn(sessionId),
|
|
558
715
|
...verifyOutputCheck !== void 0 && { verifyOutputCheck },
|
|
716
|
+
...describe !== void 0 && { describe },
|
|
559
717
|
...now !== void 0 && { now }
|
|
560
718
|
});
|
|
561
719
|
report(onVerdict, toolName, verdict);
|
|
@@ -565,6 +723,8 @@ function createActionGate(config) {
|
|
|
565
723
|
}
|
|
566
724
|
|
|
567
725
|
export {
|
|
726
|
+
emailRecipientFields,
|
|
727
|
+
emailSubjectFields,
|
|
568
728
|
outputCheckFromResult,
|
|
569
729
|
outputTextDigest,
|
|
570
730
|
felsparOutputCheckVerifier,
|
|
@@ -572,11 +732,11 @@ export {
|
|
|
572
732
|
entityIdOf,
|
|
573
733
|
decisionFromResult,
|
|
574
734
|
createDecisionRecorder,
|
|
735
|
+
felsparJobDescriber,
|
|
575
736
|
sharedDomains,
|
|
576
737
|
isCompanyDomain,
|
|
577
738
|
felsparVerifier,
|
|
578
|
-
|
|
579
|
-
emailSubjectFields,
|
|
739
|
+
denialText,
|
|
580
740
|
gatedRule,
|
|
581
741
|
checkAction,
|
|
582
742
|
createActionGate
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
denialText,
|
|
2
3
|
entityIdOf,
|
|
3
4
|
jobOf
|
|
4
|
-
} from "./chunks/chunk-
|
|
5
|
-
import "./chunks/chunk-
|
|
5
|
+
} from "./chunks/chunk-IY6A2423.js";
|
|
6
|
+
import "./chunks/chunk-HACK7D24.js";
|
|
6
7
|
|
|
7
8
|
// src/draft-action-gate-claude.ts
|
|
8
9
|
function felsparPreToolUse(gate) {
|
|
@@ -19,7 +20,7 @@ function felsparPreToolUse(gate) {
|
|
|
19
20
|
hookSpecificOutput: {
|
|
20
21
|
hookEventName: "PreToolUse",
|
|
21
22
|
permissionDecision: "deny",
|
|
22
|
-
permissionDecisionReason: `Felspar's gate: ${verdict
|
|
23
|
+
permissionDecisionReason: `Felspar's gate: ${denialText(verdict)}`
|
|
23
24
|
}
|
|
24
25
|
};
|
|
25
26
|
};
|
|
@@ -1,52 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunks/chunk-
|
|
5
|
-
import "./chunks/chunk-
|
|
6
|
-
|
|
7
|
-
// src/draft-action-gate-eve.ts
|
|
8
|
-
function felsparGate(gate) {
|
|
9
|
-
return async (call) => {
|
|
10
|
-
if (!gate.isGated(call.toolName)) {
|
|
11
|
-
return "not-applicable";
|
|
12
|
-
}
|
|
13
|
-
const verdict = await gate.check(call.session.id, call.toolName, call.toolInput);
|
|
14
|
-
return { type: verdict.allow ? "approved" : "denied", reason: verdict.reason };
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function felsparDecisionEvents(gate) {
|
|
18
|
-
const checks = /* @__PURE__ */ new Map();
|
|
19
|
-
function requested(event) {
|
|
20
|
-
const { actions } = event.data;
|
|
21
|
-
if (actions === void 0) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
for (const action of actions) {
|
|
25
|
-
const entity = entityIdOf(action.input);
|
|
26
|
-
if (entity !== void 0 && gate.isDecisionTool(action.toolName ?? "")) {
|
|
27
|
-
gate.decisions.noteRequest(action.callId, entity);
|
|
28
|
-
}
|
|
29
|
-
const job = jobOf(action.input);
|
|
30
|
-
if (job !== void 0 && gate.isOutputCheckTool(action.toolName ?? "")) {
|
|
31
|
-
checks.set(action.callId, job);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function result(event, context) {
|
|
36
|
-
const action = event.data.result;
|
|
37
|
-
const job = action?.callId === void 0 ? void 0 : checks.get(action.callId);
|
|
38
|
-
if (job !== void 0 && action?.callId !== void 0) {
|
|
39
|
-
checks.delete(action.callId);
|
|
40
|
-
if (action.isError !== true) {
|
|
41
|
-
gate.decisions.recordOutputCheck(context.session.id, job, action.output);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (action?.callId !== void 0 && gate.isDecisionTool(action.toolName ?? "") && action.isError !== true) {
|
|
45
|
-
gate.decisions.noteResult(context.session.id, action.callId, action.output);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { "actions.requested": requested, "action.result": result };
|
|
49
|
-
}
|
|
2
|
+
felsparDecisionEvents,
|
|
3
|
+
felsparGate
|
|
4
|
+
} from "./chunks/chunk-247XZ6IY.js";
|
|
5
|
+
import "./chunks/chunk-IY6A2423.js";
|
|
6
|
+
import "./chunks/chunk-HACK7D24.js";
|
|
50
7
|
export {
|
|
51
8
|
felsparDecisionEvents,
|
|
52
9
|
felsparGate
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
denialText,
|
|
2
3
|
entityIdOf,
|
|
3
4
|
jobOf
|
|
4
|
-
} from "./chunks/chunk-
|
|
5
|
-
import "./chunks/chunk-
|
|
5
|
+
} from "./chunks/chunk-IY6A2423.js";
|
|
6
|
+
import "./chunks/chunk-HACK7D24.js";
|
|
6
7
|
|
|
7
8
|
// src/draft-action-gate-openai.ts
|
|
8
9
|
function argumentsOf(text) {
|
|
@@ -30,7 +31,7 @@ function felsparToolInputGuardrail(gate, options) {
|
|
|
30
31
|
return verdict.allow ? { behavior: { type: "allow" }, outputInfo: { reason: verdict.reason } } : {
|
|
31
32
|
behavior: {
|
|
32
33
|
type: "rejectContent",
|
|
33
|
-
message: `Felspar's gate denied this call: ${verdict
|
|
34
|
+
message: `Felspar's gate denied this call: ${denialText(verdict)}`
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
37
|
}
|
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
createActionGate,
|
|
4
4
|
createDecisionRecorder,
|
|
5
5
|
decisionFromResult,
|
|
6
|
+
denialText,
|
|
6
7
|
emailRecipientFields,
|
|
7
8
|
emailSubjectFields,
|
|
8
9
|
entityIdOf,
|
|
10
|
+
felsparJobDescriber,
|
|
9
11
|
felsparOutputCheckVerifier,
|
|
10
12
|
felsparVerifier,
|
|
11
13
|
gatedRule,
|
|
@@ -14,16 +16,18 @@ import {
|
|
|
14
16
|
outputCheckFromResult,
|
|
15
17
|
outputTextDigest,
|
|
16
18
|
sharedDomains
|
|
17
|
-
} from "./chunks/chunk-
|
|
18
|
-
import "./chunks/chunk-
|
|
19
|
+
} from "./chunks/chunk-IY6A2423.js";
|
|
20
|
+
import "./chunks/chunk-HACK7D24.js";
|
|
19
21
|
export {
|
|
20
22
|
checkAction,
|
|
21
23
|
createActionGate,
|
|
22
24
|
createDecisionRecorder,
|
|
23
25
|
decisionFromResult,
|
|
26
|
+
denialText,
|
|
24
27
|
emailRecipientFields,
|
|
25
28
|
emailSubjectFields,
|
|
26
29
|
entityIdOf,
|
|
30
|
+
felsparJobDescriber,
|
|
27
31
|
felsparOutputCheckVerifier,
|
|
28
32
|
felsparVerifier,
|
|
29
33
|
gatedRule,
|