@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.
@@ -1,6 +1,119 @@
1
1
  import {
2
2
  createHostedFelsparClient
3
- } from "./chunk-C2VMKF3S.js";
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 isText = row.artifactDigest === digest;
191
+ const isText2 = row.artifactDigest === digest;
79
192
  const isDecision = row.decisionReceiptDigest === call.decisionReceiptDigest;
80
193
  const isPass = row.status === "pass";
81
- if (isPass && isText && isDecision && isOutput) {
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
- var emailRecipientFields = ["to", "cc", "bcc", "replyTo", "reply_to"];
360
- var emailSubjectFields = ["subject"];
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
- var sameField = (names) => {
366
- const lower = new Set(names.map((name) => name.toLowerCase()));
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.toLowerCase() !== company) {
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 verifiedFor(scope, found, request) {
471
- const { rules, rule } = scope;
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
- if (verified.outcome === null || !rule.outcomes.includes(verified.outcome)) {
486
- const decided = verified.outcome ?? "undecided";
487
- return { allow: false, reason: `${verified.entityId} is ${decided}, which doesn't allow this` };
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
- if (rule.outputCheck !== void 0) {
490
- const checked = await outputCheckProblem(rule.outputCheck, {
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 ${verified.outcome}; Felspar reproduced the receipt`,
503
- decision: { ...decision, outcome: verified.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 entities = [...new Set(decisions.map((row) => row.entity.toLowerCase()))].filter((entity) => isCompanyDomain(entity, rules.sharedDomains));
518
- const scope = { rules, rule, entities };
519
- const { company, reason } = companyFor(scope, input);
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 { allow: false, reason: reason ?? "no company" };
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.toLowerCase() === company).at(-1);
524
- return decision === void 0 ? { allow: false, reason: `no decision for ${company}` } : await verifiedFor(scope, { company, decision, input }, request);
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
- return await decide(request);
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
- emailRecipientFields,
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-6EZBI7O6.js";
5
- import "./chunks/chunk-C2VMKF3S.js";
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.reason}`
23
+ permissionDecisionReason: `Felspar's gate: ${denialText(verdict)}`
23
24
  }
24
25
  };
25
26
  };
@@ -1,52 +1,9 @@
1
1
  import {
2
- entityIdOf,
3
- jobOf
4
- } from "./chunks/chunk-6EZBI7O6.js";
5
- import "./chunks/chunk-C2VMKF3S.js";
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-6EZBI7O6.js";
5
- import "./chunks/chunk-C2VMKF3S.js";
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.reason}`
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-6EZBI7O6.js";
18
- import "./chunks/chunk-C2VMKF3S.js";
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,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FelsparRequestError,
3
3
  createHostedFelsparClient
4
- } from "./chunks/chunk-C2VMKF3S.js";
4
+ } from "./chunks/chunk-HACK7D24.js";
5
5
  export {
6
6
  FelsparRequestError,
7
7
  createHostedFelsparClient