@codyswann/lisa 4.74.5 → 4.74.7

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.
Files changed (67) hide show
  1. package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +52 -23
  2. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1 -1
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +6 -0
  5. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  6. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +6 -4
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/package.json +4 -4
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/SKILL.md +1 -0
  14. package/plugins/lisa/skills/lisa-setup-github-repo/SKILL.md +1 -0
  15. package/plugins/lisa-agy/plugin.json +1 -1
  16. package/plugins/lisa-agy/skills/lisa-setup-github-repo/SKILL.md +1 -0
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/skills/lisa-setup-github-repo/SKILL.md +1 -0
  24. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/skills/lisa-setup-github-repo/SKILL.md +1 -0
  26. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-agy/plugin.json +1 -1
  29. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  49. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails-agy/plugin.json +1 -1
  54. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  59. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  64. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/src/base/skills/lisa-setup-github-repo/SKILL.md +1 -0
  67. package/scripts/lisa-github-rulesets.sh +219 -0
@@ -724,6 +724,209 @@ report_removed_contexts() {
724
724
  done < <(ruleset_removed_contexts "$live" "$payload")
725
725
  }
726
726
 
727
+ ##############################################################################
728
+ # Bypass Actors
729
+ ##############################################################################
730
+
731
+ # Bypass actors follow the same contract as required checks (#4297).
732
+ #
733
+ # The applier used to send each template's `bypass_actors` as the whole list,
734
+ # and GitHub REPLACES the list on update. So a bypass a consumer added on
735
+ # purpose — measured: an admin RepositoryRole on `nightly e2e health` — was
736
+ # deleted by every apply, reported as a routine "Applied", and no config key
737
+ # could keep it. Losing a bypass is not a tightening anyone asked for either:
738
+ # it can strand the only actor allowed to land an emergency fix.
739
+ #
740
+ # So, exactly like the required list:
741
+ # - by default the LIVE bypass list is preserved and the template's entries
742
+ # are added to it;
743
+ # - a ruleset whose bypass list is DECLARED gets precisely that list, which
744
+ # is the only way to remove an actor;
745
+ # - every actor added or removed, and every bypass_mode changed, is printed
746
+ # by name, on a dry run too; an undeclared live entry keeps its own mode.
747
+ #
748
+ # Declared per ruleset under `github.rulesets.bypassActors`:
749
+ # { "github": { "rulesets": { "bypassActors": {
750
+ # "nightly e2e health": [
751
+ # { "actor_type": "DeployKey", "actor_id": null, "bypass_mode": "always" }
752
+ # ] } } } }
753
+ #
754
+ # The generated `base` ruleset already has a declaration surface —
755
+ # `policy.ruleset.bypass_actors`, which lisa-ruleset-payload.mjs builds it
756
+ # from — so that is what makes base declarative. A second key for the same
757
+ # field would recreate the two-writers problem the generator exists to end.
758
+
759
+ # The declared bypass list for one ruleset, or "null" when none is declared.
760
+ config_bypass_actors() {
761
+ local project_path="$1"
762
+ local ruleset_name="$2"
763
+ local config="$project_path/.lisa.config.json"
764
+
765
+ if [[ ! -f "$config" ]]; then
766
+ echo "null"
767
+ return 0
768
+ fi
769
+
770
+ local filter='.github.rulesets.bypassActors[$name] // null'
771
+ if [[ "$ruleset_name" == "base" ]]; then
772
+ if jq -e '.github.rulesets.bypassActors.base != null' "$config" &> /dev/null; then
773
+ log_warning "github.rulesets.bypassActors.base is ignored — declare base's bypass list as policy.ruleset.bypass_actors" >&2
774
+ fi
775
+ filter='.policy.ruleset.bypass_actors // null'
776
+ fi
777
+
778
+ # An omitted bypass_mode is GitHub's default, `always`, and an omitted
779
+ # actor_id is null (DeployKey). Filled in here so a declaration that leaves
780
+ # them out compares equal to what GitHub stores, rather than reporting a
781
+ # change that never happens.
782
+ local declared
783
+ if ! declared=$(jq -c --arg name "$ruleset_name" \
784
+ "($filter) | if type == \"array\" then map(. + {actor_id: (.actor_id // null), bypass_mode: (.bypass_mode // \"always\")}) else null end" \
785
+ "$config" 2>/dev/null); then
786
+ log_warning ".lisa.config.json could not be parsed — ignoring github.rulesets overrides" >&2
787
+ echo "null"
788
+ return 0
789
+ fi
790
+ echo "$declared"
791
+ }
792
+
793
+ # Print why this ruleset's bypass declaration is unusable, or nothing when it
794
+ # is absent or well-formed. A malformed declaration must stop the ruleset: read
795
+ # as "nothing declared" it would quietly PRESERVE the actors the operator tried
796
+ # to remove, and the run would still report success.
797
+ bypass_declaration_error() {
798
+ local project_path="$1"
799
+ local ruleset_name="$2"
800
+ local config="$project_path/.lisa.config.json"
801
+
802
+ [[ -f "$config" ]] || return 0
803
+
804
+ local key="github.rulesets.bypassActors[\"$ruleset_name\"]"
805
+ local filter='.github.rulesets.bypassActors[$name]?'
806
+ if [[ "$ruleset_name" == "base" ]]; then
807
+ key="policy.ruleset.bypass_actors"
808
+ filter='.policy.ruleset.bypass_actors?'
809
+ fi
810
+
811
+ jq -r --arg name "$ruleset_name" --arg key "$key" "
812
+ ($filter) as \$declared
813
+ | if \$declared == null then empty
814
+ elif (\$declared | type) != \"array\" then
815
+ \"\(\$key) must be a list of bypass actors, got \(\$declared | type)\"
816
+ elif any(\$declared[]; type != \"object\" or (.actor_type | type) != \"string\") then
817
+ \"\(\$key) has an entry without a string actor_type\"
818
+ elif any(\$declared[]; .actor_id != null and ((.actor_id | type) != \"number\" or .actor_id != (.actor_id | floor))) then
819
+ \"\(\$key) has an actor_id that is neither an integer nor null\"
820
+ elif any(\$declared[]; .bypass_mode as \$mode | \$mode != null and ([\"always\", \"pull_request\", \"exempt\"] | index(\$mode)) == null) then
821
+ \"\(\$key) has a bypass_mode other than always, pull_request or exempt\"
822
+ else empty end" "$config" 2>/dev/null \
823
+ || echo "$key could not be validated — refusing to apply a declaration nobody checked"
824
+ }
825
+
826
+ # Replace a template's bypass list with the declared one, when there is one.
827
+ # `base` is skipped: its payload is already built from its declaration.
828
+ apply_config_bypass_actors() {
829
+ local json="$1"
830
+ local project_path="$2"
831
+ local ruleset_name="$3"
832
+
833
+ local declared
834
+ declared=$(config_bypass_actors "$project_path" "$ruleset_name")
835
+ if [[ "$declared" == "null" || "$ruleset_name" == "base" ]]; then
836
+ echo "$json"
837
+ return 0
838
+ fi
839
+ echo "$json" | jq --argjson declared "$declared" '.bypass_actors = $declared'
840
+ }
841
+
842
+ # Union the live bypass list back into the payload. Live entries are kept
843
+ # verbatim and in live order, so a ruleset that needs no change compares equal
844
+ # and nothing is sent; template-only actors are appended. A live entry the
845
+ # template also names keeps its LIVE bypass_mode: an operator who narrowed the
846
+ # admin bypass from `always` to `pull_request` made the same deliberate choice
847
+ # as one who added it, and taking the template's mode would widen it back
848
+ # without a word. Changing a mode, like removing an actor, needs a declaration.
849
+ preserve_live_bypass_actors() {
850
+ local live="$1"
851
+ local json="$2"
852
+
853
+ if [[ -z "$live" ]] || ! echo "$live" | jq -e 'has("bypass_actors")' &> /dev/null; then
854
+ echo "$json"
855
+ return 0
856
+ fi
857
+
858
+ echo "$json" | jq --argjson live "$live" '
859
+ def actor_key: [.actor_type, .actor_id];
860
+ (.bypass_actors // []) as $want
861
+ | ($live.bypass_actors // []) as $have
862
+ | .bypass_actors = (
863
+ $have
864
+ + [ $want[]
865
+ | . as $entry
866
+ | select([ $have[] | actor_key ] | index([$entry | actor_key]) | not) ]
867
+ )'
868
+ }
869
+
870
+ # One line per bypass actor in $1's list that $2's list lacks, keyed by
871
+ # actor type and id. Formatted `<type> [<id>] (<bypass_mode>)`.
872
+ bypass_actors_missing_from() {
873
+ local from="$1"
874
+ local against="$2"
875
+
876
+ jq -r -n --argjson from "${from:-null}" --argjson against "${against:-null}" '
877
+ def actor_key: [.actor_type, .actor_id];
878
+ def actors: if . == null then [] else (.bypass_actors // []) end;
879
+ ($against | actors | map(actor_key)) as $keys
880
+ | ($from | actors)[]
881
+ | select(actor_key as $k | ($keys | index([$k])) | not)
882
+ | "\(.actor_type)\(if .actor_id == null then "" else " \(.actor_id)" end) (\(.bypass_mode))"'
883
+ }
884
+
885
+ # One line per actor present in both lists whose bypass_mode differs,
886
+ # formatted `<type> [<id>] (<live mode> -> <payload mode>)`.
887
+ bypass_actor_mode_changes() {
888
+ local live="$1"
889
+ local payload="$2"
890
+
891
+ jq -r -n --argjson live "${live:-null}" --argjson want "${payload:-null}" '
892
+ def actor_key: [.actor_type, .actor_id];
893
+ def actors: if . == null then [] else (.bypass_actors // []) end;
894
+ ($live | actors) as $have
895
+ | ($want | actors)[]
896
+ | . as $entry
897
+ | ([ $have[] | select(actor_key == ($entry | actor_key)) ] | first) as $old
898
+ | select($old != null and $old.bypass_mode != $entry.bypass_mode)
899
+ | "\(.actor_type)\(if .actor_id == null then "" else " \(.actor_id)" end) (\($old.bypass_mode) -> \(.bypass_mode))"'
900
+ }
901
+
902
+ # Name every bypass actor this run adds and removes. A removal is a warning:
903
+ # it can only happen when config declared the list, and it should still never
904
+ # read in the audit log as a routine reconciliation.
905
+ report_bypass_actor_changes() {
906
+ local live="$1"
907
+ local payload="$2"
908
+
909
+ # A live ruleset that does not disclose its bypass list cannot be preserved,
910
+ # and the payload's list will replace whatever is there.
911
+ if [[ -n "$live" ]] \
912
+ && ! echo "$live" | jq -e 'has("bypass_actors")' &> /dev/null \
913
+ && echo "$payload" | jq -e 'has("bypass_actors")' &> /dev/null; then
914
+ log_warning " GitHub did not disclose this ruleset's bypass actors — any it has will be replaced by the list sent (the token needs repository admin to read them)"
915
+ return 0
916
+ fi
917
+
918
+ local actor
919
+ while IFS= read -r actor; do
920
+ [[ -n "$actor" ]] && log_info " + bypass actor added: $actor"
921
+ done < <(bypass_actors_missing_from "$payload" "$live")
922
+ while IFS= read -r actor; do
923
+ [[ -n "$actor" ]] && log_warning " - bypass actor removed: $actor (the declared bypass list does not name it)"
924
+ done < <(bypass_actors_missing_from "$live" "$payload")
925
+ while IFS= read -r actor; do
926
+ [[ -n "$actor" ]] && log_warning " ~ bypass actor mode changed: $actor (the declared bypass list sets it)"
927
+ done < <(bypass_actor_mode_changes "$live" "$payload")
928
+ }
929
+
727
930
  apply_ruleset() {
728
931
  local repo="$1"
729
932
  local template_file="$2"
@@ -736,12 +939,20 @@ apply_ruleset() {
736
939
  local ruleset_name
737
940
  ruleset_name=$(echo "$template_content" | jq -r '.name')
738
941
 
942
+ local bypass_error
943
+ bypass_error=$(bypass_declaration_error "$project_path" "$ruleset_name")
944
+ if [[ -n "$bypass_error" ]]; then
945
+ log_error "Not applying ruleset '$ruleset_name': $bypass_error"
946
+ return 1
947
+ fi
948
+
739
949
  local clean_template
740
950
  clean_template=$(strip_readonly_fields "$template_content")
741
951
  clean_template=$(add_derived_run_gate_checks "$clean_template" "$project_path" "$ruleset_name")
742
952
  clean_template=$(add_config_required_checks "$clean_template" "$project_path" "$ruleset_name")
743
953
  clean_template=$(strip_actions_checks_if_no_workflows "$clean_template" "$project_path")
744
954
  clean_template=$(strip_config_dropped_checks "$clean_template" "$project_path")
955
+ clean_template=$(apply_config_bypass_actors "$clean_template" "$project_path" "$ruleset_name")
745
956
 
746
957
  # A template whose rules were entirely stripped has nothing to enforce here.
747
958
  if [[ $(echo "$clean_template" | jq '(.rules // [1]) | length') -eq 0 ]]; then
@@ -779,6 +990,12 @@ apply_ruleset() {
779
990
  else
780
991
  clean_template=$(preserve_live_required_checks "$live" "$clean_template")
781
992
  fi
993
+ # The same opt-in, for the bypass list: preserved unless declared.
994
+ if [[ "$(config_bypass_actors "$project_path" "$ruleset_name")" == "null" ]]; then
995
+ clean_template=$(preserve_live_bypass_actors "$live" "$clean_template")
996
+ else
997
+ log_verbose "Bypass actors for '$ruleset_name' are declared in .lisa.config.json — the live list is not preserved"
998
+ fi
782
999
  fi
783
1000
 
784
1001
  # Idempotence: a run that would change nothing must say so and send nothing,
@@ -797,6 +1014,7 @@ apply_ruleset() {
797
1014
  fi
798
1015
  report_added_contexts "$live" "$clean_template"
799
1016
  report_removed_contexts "$live" "$clean_template"
1017
+ report_bypass_actor_changes "$live" "$clean_template"
800
1018
  log_verbose "Template: $template_file"
801
1019
  return 0
802
1020
  fi
@@ -811,6 +1029,7 @@ apply_ruleset() {
811
1029
  if apply_with_integration_fallback "$repo" "$ruleset_name" "$clean_template" "$existing_id"; then
812
1030
  report_added_contexts "$live" "${APPLIED_PAYLOAD:-$clean_template}"
813
1031
  report_removed_contexts "$live" "${APPLIED_PAYLOAD:-$clean_template}"
1032
+ report_bypass_actor_changes "$live" "${APPLIED_PAYLOAD:-$clean_template}"
814
1033
  return 0
815
1034
  fi
816
1035
  log_error "Failed to apply ruleset '$ruleset_name'"