@foldspace_npm/harness 0.1.33 → 0.1.34

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,7 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { listSourceFiles, parseFile, shouldSkipActionFile } from "./ast.mjs";
4
- import { lintActionFile, lintViewsFile, ruleNoTestModeInBundle, ruleRegistryIntegrity } from "./rules.mjs";
4
+ import {
5
+ lintActionFile,
6
+ lintViewsFile,
7
+ ruleNoTestModeInBundle,
8
+ ruleRegistryIntegrity,
9
+ ruleStartersLimit,
10
+ } from "./rules.mjs";
11
+ import { lintSafetyFile, readProjectFacts, ruleNoSecrets } from "./safety.mjs";
5
12
 
6
13
  export { bundleTurnsOnTestMode } from "./rules.mjs";
7
14
 
@@ -47,12 +54,25 @@ export function lintProject(projectDir) {
47
54
  }
48
55
 
49
56
  // Any file under agent/ can end up in the bundle, including code that runs
50
- // as soon as the bundle loads, so this rule reads all of it - not only
57
+ // as soon as the bundle loads, so these rules read all of it - not only
51
58
  // actions and views.
52
59
  for (const filePath of listSourceFiles(path.join(projectDir, "agent"))) {
53
- ruleNoTestModeInBundle(parseFile(filePath), path.relative(projectDir, filePath), findings);
60
+ const sourceFile = parseFile(filePath);
61
+ const relative = path.relative(projectDir, filePath);
62
+ ruleNoTestModeInBundle(sourceFile, relative, findings);
63
+ ruleStartersLimit(sourceFile, relative, findings);
54
64
  }
55
65
 
66
+ // Safety: every file under agent/ the project owns - not agent/runtime/,
67
+ // which is the harness's own copy - and, for secrets, the whole project.
68
+ const facts = readProjectFacts(projectDir);
69
+ for (const filePath of listSourceFiles(path.join(projectDir, "agent"))) {
70
+ const relative = path.relative(projectDir, filePath);
71
+ if (relative.split(path.sep)[1] === "runtime") continue;
72
+ lintSafetyFile(filePath, relative, findings, facts);
73
+ }
74
+ ruleNoSecrets(projectDir, findings);
75
+
56
76
  ruleRegistryIntegrity({
57
77
  indexFile: entry,
58
78
  indexSource: parseFile(entry),
@@ -22,9 +22,11 @@ import {
22
22
  import { ts } from "./ts.mjs";
23
23
 
24
24
  /*
25
+ Safety rules (one customer's address, the user's token, secrets, known
26
+ hosts) live in ./safety.mjs.
27
+
25
28
  Backlog, not this slice:
26
- - no-hardcoded-secrets
27
- - no-unsafe-return (stack / cookies / Authorization / ApiResult.detail)
29
+ - no-unsafe-return (stack / ApiResult.detail)
28
30
  - snake_case action keys
29
31
  - credentials: "include" on every fetch
30
32
  - _gen2 vs _v2 taskKey
@@ -804,6 +806,48 @@ export function ruleNoTestModeInBundle(sourceFile, relative, findings) {
804
806
  walk(sourceFile, undefined);
805
807
  }
806
808
 
809
+ // The most starters one page (or the agent-wide default) shows, both groups
810
+ // together. `setConversationStarters` also sets the panel's opening screen,
811
+ // and a long list there buries the one that matters (a customer default had 7).
812
+ export const MAX_STARTERS = 3;
813
+
814
+ // Any object with a KNOWLEDGE or ACTION list - a setConversationStarters
815
+ // call, a page's entry in the bottom bar's list, conversationStartersSettings
816
+ // - is counted. Only lists written out in the file are counted: a list built
817
+ // at run time (`.map`, a spread) is left alone.
818
+ export function ruleStartersLimit(sourceFile, relative, findings) {
819
+ const bindings = collectModuleBindings(sourceFile);
820
+ const visit = (node) => {
821
+ if (ts.isObjectLiteralExpression(node)) {
822
+ let count = 0;
823
+ let counted = false;
824
+ for (const prop of objectProperties(node)) {
825
+ const name = propertyName(prop);
826
+ if (name !== "KNOWLEDGE" && name !== "ACTION") continue;
827
+ const list = resolveNode(propertyValue(prop), bindings);
828
+ if (!list || !ts.isArrayLiteralExpression(list)) continue;
829
+ counted = true;
830
+ count += list.elements.filter((element) => !ts.isSpreadElement(element)).length;
831
+ }
832
+ if (counted && count > MAX_STARTERS) {
833
+ findings.push(
834
+ finding({
835
+ rule: "starters-limit",
836
+ severity: "error",
837
+ sourceFile,
838
+ node,
839
+ file: relative,
840
+ message: `${count} conversation starters in one set; at most ${MAX_STARTERS}, KNOWLEDGE and ACTION together. The same list is the agent panel's opening screen.`,
841
+ fix: `Keep the ${MAX_STARTERS} that matter most on this page - each one something the agent can answer today.`,
842
+ }),
843
+ );
844
+ }
845
+ }
846
+ ts.forEachChild(node, visit);
847
+ };
848
+ visit(sourceFile);
849
+ }
850
+
807
851
  // What the build checks in the bundle it wrote: minify keeps property names,
808
852
  // so a bundle that can turn test mode on still says `setTestMode`. Calls with
809
853
  // `false` (`!1` once minified) are allowed; any other mention is not - the