@codyswann/lisa 4.27.6 → 4.27.8
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/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +507 -11
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +102 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/project-config-nightly-e2e.d.ts +21 -0
- package/dist/core/project-config-nightly-e2e.d.ts.map +1 -0
- package/dist/core/project-config-nightly-e2e.js +43 -0
- package/dist/core/project-config-nightly-e2e.js.map +1 -0
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +3 -0
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +56 -12
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +180 -19
- package/expo/create-only/.github/nightly-e2e-policy.json +16 -0
- package/expo/create-only/.github/workflows/nightly-e2e-health.yml +33 -8
- package/expo/create-only/.github/workflows/nightly-e2e-tracking.yml +30 -0
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/lisa/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-agy/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-agy/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/lisa-copilot/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-copilot/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-copilot/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/lisa-cursor/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-cursor/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-cursor/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-direct-issue-create.sh +507 -11
- package/plugins/src/base/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/src/base/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/src/base/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +10 -6
- package/scripts/two-channel-couplings.json +70 -5
- package/typescript/copy-overwrite/.prettierignore +7 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-action.mjs +70 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-github-jira.mjs +296 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-linear-sentry.mjs +283 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-support.mjs +55 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-tracking-config.mjs +149 -0
- package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking-cli.mjs +56 -0
- package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking.mjs +208 -0
|
@@ -1218,17 +1218,21 @@ specific, actionable, e.g. `Examined 14 items; all active or in backoff — noth
|
|
|
1218
1218
|
`nothing-needed`):
|
|
1219
1219
|
|
|
1220
1220
|
```bash
|
|
1221
|
-
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
1221
|
+
node "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa}/scripts/automation-run-record.mjs" \
|
|
1222
1222
|
--loop-id intake-repair --outcome change-proved \
|
|
1223
1223
|
--summary "Recovered 3 stalled builds and closed out 2 rollups; all confirmed." \
|
|
1224
1224
|
--runbook .lisa/automations/intake-repair.runbook.md [--ref <item-url>]...
|
|
1225
1225
|
```
|
|
1226
1226
|
|
|
1227
|
-
|
|
1228
|
-
`
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1227
|
+
The `:-` default in that command is load-bearing, not decoration. `CLAUDE_PLUGIN_ROOT` is **not
|
|
1228
|
+
exported into an agent's Bash tool environment**, so the bare `"${CLAUDE_PLUGIN_ROOT}/scripts/…"`
|
|
1229
|
+
form expands to an absolute `/scripts/…` and exits 1. The default names the installed package copy,
|
|
1230
|
+
which resolves from the **consumer repository root** — which is where the recorder is actually
|
|
1231
|
+
invoked. Never substitute a bare `plugins/lisa/scripts/…` or `plugins/src/base/scripts/…` path:
|
|
1232
|
+
those are relative to the **Lisa package root**, not the repository you are standing in, and exit 1
|
|
1233
|
+
from there. If recording still fails, **degrade, never abort** (per `automation-runbook-contract`):
|
|
1234
|
+
note the recording failure in the run output and finish the cycle — a recording failure is a
|
|
1235
|
+
degradation to report, never a reason to block the loop.
|
|
1232
1236
|
|
|
1233
1237
|
**Retirement evaluation (every run).** The `intake-repair` loop is **structural to the
|
|
1234
1238
|
factory — it does not retire.** Its runbook says so plainly instead of leaving the Retirement
|
|
@@ -27,10 +27,24 @@
|
|
|
27
27
|
#
|
|
28
28
|
# A creation command is refused unless it carries a readiness declaration:
|
|
29
29
|
# - the project's configured build-ready role (GitHub label, JIRA/Linear
|
|
30
|
-
# workflow state) resolved from `.lisa.config.json`, never hard-coded;
|
|
30
|
+
# workflow state) resolved from `.lisa.config.json`, never hard-coded;
|
|
31
|
+
# - on a tracker whose ready role is a STATE rather than a label, the
|
|
32
|
+
# `lifecycle_role: ready` declaration the access layer resolves that state
|
|
33
|
+
# from — because no flag on the mandated client can carry a state, and a
|
|
34
|
+
# guard with no satisfiable declaration is a guard that teaches lying; or
|
|
31
35
|
# - an explicit `[lisa-human-gate]` marker, inline or in the `--body-file`
|
|
32
36
|
# the create is about to submit.
|
|
33
37
|
#
|
|
38
|
+
# WHERE IT LOOKS
|
|
39
|
+
#
|
|
40
|
+
# argv, the request payload (inline, in a `--data-binary @file`, or piped in
|
|
41
|
+
# over stdin from the same pipeline), and the contents of any file the command
|
|
42
|
+
# names. The last of those is what CodySwannGT/lisa#3484 was: the guard
|
|
43
|
+
# inspected argv and nothing else, so `bash /path/create.sh` showed it two
|
|
44
|
+
# tokens and the creation was one file away. Lisa's own `parity-safety-net.sh`
|
|
45
|
+
# tells agents to write payloads to a file and execute the file, so complying
|
|
46
|
+
# with Lisa's guidance produced the bypass.
|
|
47
|
+
#
|
|
34
48
|
# That is exactly the machine-checkable content of `ready-role-filing`, and it
|
|
35
49
|
# lets `lisa-github-write-issue` / `lisa-jira-write-ticket` /
|
|
36
50
|
# `lisa-linear-write-issue` through by construction, because those writers
|
|
@@ -194,6 +208,38 @@ fi
|
|
|
194
208
|
# environment, and never from the command being inspected.
|
|
195
209
|
ambient_override="${LISA_ALLOW_DIRECT_ISSUE_CREATE:-}"
|
|
196
210
|
|
|
211
|
+
# How to write the declaration down on THIS project's tracker. The answer is
|
|
212
|
+
# not the same everywhere, and printing the GitHub answer at a Linear operator
|
|
213
|
+
# is what made this guard unsatisfiable: it named a `--label` flag that the
|
|
214
|
+
# mandated client does not have, on a role that is not a label.
|
|
215
|
+
declaration_hint() {
|
|
216
|
+
case "$tracker" in
|
|
217
|
+
jira | linear)
|
|
218
|
+
cat <<EOF
|
|
219
|
+
Your build-ready role \`$ready_role\` is a workflow STATE, not a label, and
|
|
220
|
+
the mandated client is \`curl\` — which has no flag that carries a state. So
|
|
221
|
+
declare the LIFECYCLE ROLE the access layer resolves the state from, and let
|
|
222
|
+
it do the resolving:
|
|
223
|
+
|
|
224
|
+
LIFECYCLE_ROLE=ready curl -sS -X POST <the tracker endpoint> …
|
|
225
|
+
|
|
226
|
+
or \`lifecycle_role:ready\` in the request payload, or a \`--state\` /
|
|
227
|
+
\`--status\` flag where the CLI has one. The \`$tracker\` access layer takes
|
|
228
|
+
that role, resolves it against the tracker's own state catalog, and fails
|
|
229
|
+
CLOSED if it cannot — so the token is the input that decides the lane, not a
|
|
230
|
+
decoration.
|
|
231
|
+
EOF
|
|
232
|
+
;;
|
|
233
|
+
*)
|
|
234
|
+
cat <<EOF
|
|
235
|
+
The command has to carry the configured build-ready role \`$ready_role\` as
|
|
236
|
+
the value of a \`--label\` / \`--status\` / \`--state\` flag — not in the
|
|
237
|
+
title or body, because a role named in prose is not a role applied.
|
|
238
|
+
EOF
|
|
239
|
+
;;
|
|
240
|
+
esac
|
|
241
|
+
}
|
|
242
|
+
|
|
197
243
|
refuse() {
|
|
198
244
|
local signature="$1"
|
|
199
245
|
local roles="$2"
|
|
@@ -234,10 +280,17 @@ Filed, not ready, and no \`human_gate\` is the incomplete-handoff case, and
|
|
|
234
280
|
attached. See the \`ready-role-filing\` rule for the full contract.
|
|
235
281
|
|
|
236
282
|
If you must run the CLI directly, the command has to carry one of the two
|
|
237
|
-
declarations itself:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
283
|
+
declarations itself:
|
|
284
|
+
|
|
285
|
+
$(declaration_hint)
|
|
286
|
+
|
|
287
|
+
Or a \`[lisa-human-gate]\` marker in the body it submits, when a human
|
|
288
|
+
product call really is pending.
|
|
289
|
+
|
|
290
|
+
WHERE THE DECLARATION IS READ FROM: argv, the request payload — inline, in a
|
|
291
|
+
\`--data-binary @file\`, or piped in over stdin — and the contents of a script
|
|
292
|
+
this command runs. Moving the create into a file no longer moves it out of
|
|
293
|
+
sight, so the declaration can live wherever the create does.
|
|
241
294
|
|
|
242
295
|
OPERATOR ESCAPE: a human can export \`LISA_ALLOW_DIRECT_ISSUE_CREATE=1\` in the
|
|
243
296
|
environment before starting the session. It is deliberately not reachable by
|
|
@@ -324,6 +377,8 @@ own_repo = os.environ.get("LISA_GUARD_OWN_REPO", "").strip().lower()
|
|
|
324
377
|
upstream_repo = os.environ.get("LISA_GUARD_UPSTREAM_REPO", "").strip().lower()
|
|
325
378
|
upstream_ready_role = os.environ.get("LISA_GUARD_UPSTREAM_READY_ROLE", "")
|
|
326
379
|
caller_is_github = os.environ.get("LISA_GUARD_CALLER_IS_GITHUB", "") == "1"
|
|
380
|
+
tracker = os.environ.get("LISA_GUARD_TRACKER", "").strip().lower()
|
|
381
|
+
project_dir = os.environ.get("LISA_GUARD_PROJECT_DIR", "")
|
|
327
382
|
|
|
328
383
|
OVERRIDE_NAME = "LISA_ALLOW_DIRECT_ISSUE_CREATE"
|
|
329
384
|
HUMAN_GATE_MARKER = "[lisa-human-gate]"
|
|
@@ -410,6 +465,115 @@ GITHUB_ISSUES_URL_REPO = re.compile(
|
|
|
410
465
|
JIRA_ISSUE_URL = re.compile(r"atlassian\.net/rest/api/[^/\s]+/issue")
|
|
411
466
|
GRAPHQL_CREATE = re.compile(r"createIssue|issueCreate")
|
|
412
467
|
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
# REACH: THE COMMAND IS NOT THE ONLY TEXT THAT RUNS
|
|
470
|
+
#
|
|
471
|
+
# The classifier above asks the right question of the wrong text. It examines
|
|
472
|
+
# argv, and argv is not where a creation has to live:
|
|
473
|
+
#
|
|
474
|
+
# bash /tmp/create.sh # URL and `issueCreate` both inside the script
|
|
475
|
+
# node wrapper.mjs --state x # same, in JavaScript
|
|
476
|
+
# curl … --data-binary @p.json # URL in argv, mutation in the file
|
|
477
|
+
#
|
|
478
|
+
# All three ALLOWED, verified by driving the guard with synthetic payloads
|
|
479
|
+
# (CodySwannGT/lisa#3484). The detection is a CONJUNCTION — an endpoint token
|
|
480
|
+
# AND a creation verb in the SAME inspected command — so moving either half one
|
|
481
|
+
# file away means the conjunction never forms. `bash <path>` shows the
|
|
482
|
+
# classifier two tokens, neither of which is a tracker CLI.
|
|
483
|
+
#
|
|
484
|
+
# It is not an oversight that an operator had to be clever to find. Lisa's OWN
|
|
485
|
+
# guards instruct agents into this shape: `parity-safety-net.sh` refuses
|
|
486
|
+
# heredocs and says "write the payload to a file with the Write tool, then
|
|
487
|
+
# execute that file directly", and `block-shell-json-parsing.sh` pushes JSON
|
|
488
|
+
# construction into `jq` scripts. An agent complying perfectly with Lisa's
|
|
489
|
+
# guidance lands in the uninspected path BY DEFAULT. The guards were
|
|
490
|
+
# individually reasonable and jointly self-defeating.
|
|
491
|
+
#
|
|
492
|
+
# The fix follows this file's own inverted method. Enumerating interpreters —
|
|
493
|
+
# bash, sh, zsh, ksh, dash, python3, node, bun, deno, ruby, perl, `./script`,
|
|
494
|
+
# `source`, `env bash` — is the unbounded question all over again, and every
|
|
495
|
+
# gap in it fails OPEN. So the question is inverted the same way the wrapper
|
|
496
|
+
# question was: instead of "which programs execute their operands?", the
|
|
497
|
+
# classifier asks "does this command name a readable file whose CONTENTS read
|
|
498
|
+
# as an undeclared tracker creation?" — bounded by the tokens actually present.
|
|
499
|
+
#
|
|
500
|
+
# THE COST IS STATED, NOT HIDDEN. `cat create.sh` and `git add create.sh` are
|
|
501
|
+
# refused too, because proving that a program does NOT execute its operand
|
|
502
|
+
# requires exactly the allowlist this file already refuses to keep. That is the
|
|
503
|
+
# documented direction of failure: an operator who hits a false refusal says
|
|
504
|
+
# so, an agent who hits a false allow says nothing.
|
|
505
|
+
FILE_OPERAND_MAX_BYTES = 262144
|
|
506
|
+
FILE_OPERANDS_PER_SEGMENT = 8
|
|
507
|
+
# Payload sources. A creation's mutation text may arrive inline, from a file,
|
|
508
|
+
# or over stdin from an earlier stage of the same pipeline.
|
|
509
|
+
PAYLOAD_SOURCE_FLAGS = {
|
|
510
|
+
"-d", "--data", "--data-raw", "--data-binary", "--data-ascii",
|
|
511
|
+
"--data-urlencode", "--input", "-F", "--form", "--upload-file", "-T",
|
|
512
|
+
}
|
|
513
|
+
STDIN_PAYLOAD_TOKENS = {"-", "@-"}
|
|
514
|
+
# A tracker endpoint appearing anywhere in a file's text. Paired with
|
|
515
|
+
# GRAPHQL_CREATE as a CONJUNCTION, so an ordinary file that merely mentions
|
|
516
|
+
# `issueCreate` in prose — a changelog, a test name, a code comment — does not
|
|
517
|
+
# read as a creation on its own.
|
|
518
|
+
TRACKER_ENDPOINT = re.compile(
|
|
519
|
+
r"api\.linear\.app/graphql"
|
|
520
|
+
r"|api\.github\.com"
|
|
521
|
+
r"|atlassian\.net/rest/api"
|
|
522
|
+
r"|repos/[^/\s?#'\"]+/[^/\s?#'\"]+/issues",
|
|
523
|
+
re.IGNORECASE,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
# DECLARING BUILD-READY WHEN THE ROLE IS A STATE AND NOT A LABEL
|
|
528
|
+
#
|
|
529
|
+
# `declares_readiness` accepted the build-ready role only as the value of a
|
|
530
|
+
# LABEL_FLAGS flag in argv. That is well-formed for a LABEL-based tracker and
|
|
531
|
+
# structurally impossible for a STATE-based one:
|
|
532
|
+
#
|
|
533
|
+
# - GitHub's ready role is a label, labels are argv-native (`--label`), so
|
|
534
|
+
# `flag_values` finds it and an honest command exists.
|
|
535
|
+
# - JIRA's and Linear's ready roles are workflow STATES. The mandated access
|
|
536
|
+
# path is raw `curl` to a GraphQL/REST endpoint, `curl` has no `--state`
|
|
537
|
+
# flag, and the state lives in the request payload as an ID the guard
|
|
538
|
+
# cannot resolve without a network round-trip it refuses to make.
|
|
539
|
+
#
|
|
540
|
+
# So on a Linear-tracked project the only declaration left that passed was
|
|
541
|
+
# `[lisa-human-gate]`, which this file's own comments correctly forbid for a
|
|
542
|
+
# build-ready item: it stamps the item as held for a human product call, and
|
|
543
|
+
# build-intake scans the ready role and nothing else. An honest operator had NO
|
|
544
|
+
# compliant command and exactly one dishonest one — a guard failing in the
|
|
545
|
+
# harmful direction, where complying is worse than not.
|
|
546
|
+
#
|
|
547
|
+
# The declaration accepted here for state-based trackers is the LIFECYCLE ROLE
|
|
548
|
+
# the access layer already consumes. `lisa-linear-access` refuses to accept a
|
|
549
|
+
# caller-supplied `stateId`; it takes `lifecycle_role:<ROLE>`, resolves it
|
|
550
|
+
# against config and the team's own state catalog through
|
|
551
|
+
# `linear-state-write-target.mjs`, and fails CLOSED when it cannot. So the role
|
|
552
|
+
# token is not decoration: it is the input that decides which lane the item
|
|
553
|
+
# lands in, and a command carrying it either places the item in the ready lane
|
|
554
|
+
# or refuses. That is the same epistemic standing as `--label status:ready`,
|
|
555
|
+
# whose effect also happens one layer down inside `gh`.
|
|
556
|
+
#
|
|
557
|
+
# Scoped deliberately: accepted ONLY when the configured tracker's ready role
|
|
558
|
+
# is a state, and ONLY for a filing addressed at this project's own tracker. A
|
|
559
|
+
# GitHub filing — same-repo or cross-repo — still has to carry the label,
|
|
560
|
+
# because for GitHub the label IS expressible and a second, weaker spelling
|
|
561
|
+
# would be a hole rather than a remedy.
|
|
562
|
+
STATE_ROLE_TRACKERS = {"jira", "linear"}
|
|
563
|
+
#
|
|
564
|
+
# The optional quote AFTER the key name is load-bearing, not decoration. Both
|
|
565
|
+
# refusal messages tell the operator to put the declaration in the request
|
|
566
|
+
# payload, and a JSON payload quotes its keys — so a pattern demanding `[:=]`
|
|
567
|
+
# immediately after a bare key name refuses `"lifecycle_role": "ready"`, which
|
|
568
|
+
# is the exact spelling it just asked for. Caught by review before it shipped.
|
|
569
|
+
LIFECYCLE_ROLE_READY = re.compile(
|
|
570
|
+
r"(?:^|[^\w.-])(?:lifecycle_role|LIFECYCLE_ROLE)[\"']?\s*[:=]\s*[\"']?ready\b"
|
|
571
|
+
r"|(?:^|[^\w.-])--role[=\s]+[\"']?ready\b",
|
|
572
|
+
)
|
|
573
|
+
# Built per role at the point of use, because the role is project data. Kept
|
|
574
|
+
# as a template rather than a compiled pattern so the role is always escaped.
|
|
575
|
+
LABEL_FLAG_TEXT = r"--(?:label|labels|add-label|status|state)[=\s]+[\"']?%s(?![\w:.-])"
|
|
576
|
+
|
|
413
577
|
MAX_NESTING_DEPTH = 3
|
|
414
578
|
# Operators that can be GLUED to an adjacent word (`true&&gh issue create`),
|
|
415
579
|
# so they must be split out of a token. Braces and parentheses are deliberately
|
|
@@ -730,12 +894,208 @@ def endpoint_paths(args, pattern):
|
|
|
730
894
|
return found
|
|
731
895
|
|
|
732
896
|
|
|
733
|
-
def
|
|
897
|
+
def resolve_operand(token):
|
|
898
|
+
"""A readable regular file named by one token, or None.
|
|
899
|
+
|
|
900
|
+
Bounded on purpose. The size cap keeps a PreToolUse hook off a multi-
|
|
901
|
+
megabyte read on every intercepted command, and a file too large to
|
|
902
|
+
inspect is skipped rather than half-read: a truncated scan reports a
|
|
903
|
+
confident ALLOW about text it never saw.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
token: One argument, possibly quoted or `@`-prefixed for curl.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
An existing path, or None.
|
|
910
|
+
"""
|
|
911
|
+
text = token.strip().strip("'\"")
|
|
912
|
+
# `-d@payload.json` names a file just as plainly as `@payload.json` does.
|
|
913
|
+
attached = attached_value(text)
|
|
914
|
+
if attached is not None:
|
|
915
|
+
text = attached
|
|
916
|
+
if text.startswith("@"):
|
|
917
|
+
text = text[1:]
|
|
918
|
+
if not text or text in STDIN_PAYLOAD_TOKENS:
|
|
919
|
+
return None
|
|
920
|
+
candidates = [text]
|
|
921
|
+
if project_dir and not os.path.isabs(text):
|
|
922
|
+
candidates.append(os.path.join(project_dir, text))
|
|
923
|
+
for candidate in candidates:
|
|
924
|
+
try:
|
|
925
|
+
if not os.path.isfile(candidate):
|
|
926
|
+
continue
|
|
927
|
+
if os.path.getsize(candidate) > FILE_OPERAND_MAX_BYTES:
|
|
928
|
+
continue
|
|
929
|
+
except OSError:
|
|
930
|
+
continue
|
|
931
|
+
return candidate
|
|
932
|
+
return None
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def read_operand(path):
|
|
936
|
+
"""The text of a file the command names.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
path: A path from `resolve_operand`.
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
The contents, or an empty string when unreadable.
|
|
943
|
+
"""
|
|
944
|
+
try:
|
|
945
|
+
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
946
|
+
return handle.read(FILE_OPERAND_MAX_BYTES)
|
|
947
|
+
except OSError:
|
|
948
|
+
return ""
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def attached_value(token):
|
|
952
|
+
"""A payload flag's value when it is GLUED to the flag.
|
|
953
|
+
|
|
954
|
+
`curl -d@payload.json` and `curl -d'{"query":…}'` are one token each, so a
|
|
955
|
+
parser that only looks at `args[i + 1]` and at `flag=value` sees neither —
|
|
956
|
+
and `-d@file` is the ordinary spelling, not an exotic one. Caught by review
|
|
957
|
+
before it shipped, and it was a real bypass: the mutation stayed in the
|
|
958
|
+
file, the conjunction never formed, and an undeclared creation passed.
|
|
959
|
+
|
|
960
|
+
Longest flag first, so `--data-binary@f` is not read as `--data` with a
|
|
961
|
+
`-binary@f` value. A remainder starting with `-` is rejected for the same
|
|
962
|
+
reason: it is another option, not this one's value.
|
|
963
|
+
|
|
964
|
+
Args:
|
|
965
|
+
token: One argument.
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
The attached value, or None.
|
|
969
|
+
"""
|
|
970
|
+
for flag in sorted(PAYLOAD_SOURCE_FLAGS, key=len, reverse=True):
|
|
971
|
+
if not token.startswith(flag) or len(token) == len(flag):
|
|
972
|
+
continue
|
|
973
|
+
rest = token[len(flag) :]
|
|
974
|
+
if rest.startswith("="):
|
|
975
|
+
rest = rest[1:]
|
|
976
|
+
if not rest or rest.startswith("-"):
|
|
977
|
+
continue
|
|
978
|
+
return rest
|
|
979
|
+
return None
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def payload_text(args, whole_command):
|
|
983
|
+
"""The body this command will submit, wherever it is coming from.
|
|
984
|
+
|
|
985
|
+
Three sources, because a request body has three places to live and the
|
|
986
|
+
guard was reading only the first: inline after `-d`, in a file after
|
|
987
|
+
`--data-binary @path` / `--input path`, or on stdin from an earlier stage
|
|
988
|
+
of the same pipeline.
|
|
989
|
+
|
|
990
|
+
The stdin case takes the WHOLE command text rather than the segment,
|
|
991
|
+
because `jq -n … | curl … --data-binary @-` is one logical command that
|
|
992
|
+
`segment` has already split in two — the payload literally is the other
|
|
993
|
+
half. That is the shape `lisa-linear-access` mandates, so it has to be
|
|
994
|
+
readable rather than invisible.
|
|
995
|
+
|
|
996
|
+
Args:
|
|
997
|
+
args: A command's arguments.
|
|
998
|
+
whole_command: The full intercepted command string.
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
The payload text, possibly empty.
|
|
1002
|
+
"""
|
|
1003
|
+
parts = []
|
|
1004
|
+
for index, token in enumerate(args):
|
|
1005
|
+
value = None
|
|
1006
|
+
if token in PAYLOAD_SOURCE_FLAGS and index + 1 < len(args):
|
|
1007
|
+
value = args[index + 1]
|
|
1008
|
+
elif "=" in token:
|
|
1009
|
+
head, rhs = token.split("=", 1)
|
|
1010
|
+
if head in PAYLOAD_SOURCE_FLAGS:
|
|
1011
|
+
value = rhs
|
|
1012
|
+
if value is None:
|
|
1013
|
+
value = attached_value(token)
|
|
1014
|
+
if value is None:
|
|
1015
|
+
continue
|
|
1016
|
+
stripped = value.strip().strip("'\"")
|
|
1017
|
+
if stripped in STDIN_PAYLOAD_TOKENS:
|
|
1018
|
+
parts.append(whole_command)
|
|
1019
|
+
continue
|
|
1020
|
+
parts.append(stripped)
|
|
1021
|
+
path = resolve_operand(stripped)
|
|
1022
|
+
if path is not None:
|
|
1023
|
+
parts.append(read_operand(path))
|
|
1024
|
+
return "\n".join(parts)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def text_declares_readiness(text):
|
|
1028
|
+
"""Whether a file's or payload's own text carries a declaration.
|
|
1029
|
+
|
|
1030
|
+
The token-position machinery above cannot be reused here: a file is not
|
|
1031
|
+
argv, and a payload is JSON. What is checked is deliberately the SAME
|
|
1032
|
+
three declarations, matched textually and never loosened into "the role
|
|
1033
|
+
string appears somewhere" — a state-based ready role is an ordinary word
|
|
1034
|
+
like `Ready`, and accepting a bare occurrence of it would let prose in a
|
|
1035
|
+
description declare readiness the item does not have.
|
|
1036
|
+
|
|
1037
|
+
Args:
|
|
1038
|
+
text: File or payload contents.
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
True when the text declares build-ready or a human gate.
|
|
1042
|
+
"""
|
|
1043
|
+
if HUMAN_GATE_MARKER in text:
|
|
1044
|
+
return True
|
|
1045
|
+
for role in (ready_role, upstream_ready_role, default_ready_role):
|
|
1046
|
+
if not role:
|
|
1047
|
+
continue
|
|
1048
|
+
if re.search(LABEL_FLAG_TEXT % re.escape(role), text):
|
|
1049
|
+
return True
|
|
1050
|
+
if tracker in STATE_ROLE_TRACKERS:
|
|
1051
|
+
if LIFECYCLE_ROLE_READY.search(text):
|
|
1052
|
+
return True
|
|
1053
|
+
if ready_role and re.search(
|
|
1054
|
+
r"\"(?:state|status|stateName|statusName|state_name|name|transition)\""
|
|
1055
|
+
r"\s*:\s*\"%s\"" % re.escape(ready_role),
|
|
1056
|
+
text,
|
|
1057
|
+
re.IGNORECASE,
|
|
1058
|
+
):
|
|
1059
|
+
return True
|
|
1060
|
+
return False
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def scope_declaration(text, state_role_ok):
|
|
1064
|
+
"""Whether a whole text declares readiness for everything inside it.
|
|
1065
|
+
|
|
1066
|
+
Only the two MARKERS qualify, never the `--label` flag, and that asymmetry
|
|
1067
|
+
is the same one the argv check already makes. A marker has no other
|
|
1068
|
+
meaning, so its presence anywhere in a script IS the declaration for that
|
|
1069
|
+
script. A label flag is positional and belongs to one create; letting it
|
|
1070
|
+
vouch for a second, unlabelled create further down the same file would be a
|
|
1071
|
+
hole rather than a convenience.
|
|
1072
|
+
|
|
1073
|
+
`state_role_ok` carries the SAME scoping the argv path applies, and it has
|
|
1074
|
+
to be threaded here rather than recomputed from the tracker alone: a Linear
|
|
1075
|
+
project whose script files `gh issue create --repo <other>` is a cross-repo
|
|
1076
|
+
GitHub filing, and this project's workflow role does not answer for another
|
|
1077
|
+
repository's queue. Checking only `tracker in STATE_ROLE_TRACKERS` waved
|
|
1078
|
+
exactly that through. Caught by review before it shipped.
|
|
1079
|
+
|
|
1080
|
+
Args:
|
|
1081
|
+
text: A script's contents, or one command segment.
|
|
1082
|
+
state_role_ok: Whether a lifecycle-role declaration answers here.
|
|
1083
|
+
|
|
1084
|
+
Returns:
|
|
1085
|
+
True when the text carries a whole-scope declaration.
|
|
1086
|
+
"""
|
|
1087
|
+
if HUMAN_GATE_MARKER in text:
|
|
1088
|
+
return True
|
|
1089
|
+
return bool(state_role_ok and LIFECYCLE_ROLE_READY.search(text))
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def creation_signature(name, args, extra=""):
|
|
734
1093
|
"""Classify a tracker CLI invocation as a creation.
|
|
735
1094
|
|
|
736
1095
|
Args:
|
|
737
1096
|
name: The CLI basename.
|
|
738
1097
|
args: Every token after it in this segment.
|
|
1098
|
+
extra: Payload text this command submits, from a file or stdin.
|
|
739
1099
|
|
|
740
1100
|
Returns:
|
|
741
1101
|
A short human-readable signature, or None.
|
|
@@ -743,6 +1103,8 @@ def creation_signature(name, args):
|
|
|
743
1103
|
if "--help" in args or "-h" in args:
|
|
744
1104
|
return None
|
|
745
1105
|
joined = " ".join(args)
|
|
1106
|
+
if extra:
|
|
1107
|
+
joined = joined + "\n" + extra
|
|
746
1108
|
|
|
747
1109
|
if name == "gh":
|
|
748
1110
|
if invokes_verb(args, {"issue"}, "create"):
|
|
@@ -930,12 +1292,16 @@ def roles_for(target):
|
|
|
930
1292
|
return [ready_role, role], None
|
|
931
1293
|
|
|
932
1294
|
|
|
933
|
-
def declares_readiness(raw_args, roles):
|
|
934
|
-
"""Whether the create carries one of the
|
|
1295
|
+
def declares_readiness(raw_args, roles, extra="", state_role_ok=False):
|
|
1296
|
+
"""Whether the create carries one of the required declarations.
|
|
935
1297
|
|
|
936
1298
|
Args:
|
|
937
1299
|
raw_args: The creating command's arguments.
|
|
938
1300
|
roles: The build-ready role tokens that satisfy this filing.
|
|
1301
|
+
extra: Payload text this command submits, from a file or stdin.
|
|
1302
|
+
state_role_ok: Whether a lifecycle-role declaration answers here. True
|
|
1303
|
+
only for a state-based tracker filing into its own tracker, where
|
|
1304
|
+
no argv flag on the mandated client can carry the state.
|
|
939
1305
|
|
|
940
1306
|
Returns:
|
|
941
1307
|
True when a build-ready role or a human-gate marker is present.
|
|
@@ -961,6 +1327,16 @@ def declares_readiness(raw_args, roles):
|
|
|
961
1327
|
return True
|
|
962
1328
|
except OSError:
|
|
963
1329
|
continue
|
|
1330
|
+
if extra and HUMAN_GATE_MARKER in extra:
|
|
1331
|
+
return True
|
|
1332
|
+
# The state-based path. Scoped by `state_role_ok` rather than checked
|
|
1333
|
+
# unconditionally, so this adds a compliant command where none existed and
|
|
1334
|
+
# takes none away where one already did.
|
|
1335
|
+
if state_role_ok:
|
|
1336
|
+
if LIFECYCLE_ROLE_READY.search(" ".join(args)):
|
|
1337
|
+
return True
|
|
1338
|
+
if extra and text_declares_readiness(extra):
|
|
1339
|
+
return True
|
|
964
1340
|
return False
|
|
965
1341
|
|
|
966
1342
|
|
|
@@ -1016,12 +1392,81 @@ def nested_operands(argv):
|
|
|
1016
1392
|
return operands
|
|
1017
1393
|
|
|
1018
1394
|
|
|
1019
|
-
|
|
1395
|
+
inspected_files = set()
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def file_operands(argv):
|
|
1399
|
+
"""Files this command names, in the order they appear.
|
|
1400
|
+
|
|
1401
|
+
Every token is offered, and the ones that name a readable regular file are
|
|
1402
|
+
kept. Deciding WHICH programs execute their operands is the unbounded
|
|
1403
|
+
question this file already refuses to ask, so it is not asked here either.
|
|
1404
|
+
|
|
1405
|
+
Args:
|
|
1406
|
+
argv: One command's tokens.
|
|
1407
|
+
|
|
1408
|
+
Returns:
|
|
1409
|
+
Existing paths, deduplicated across the whole scan and capped.
|
|
1410
|
+
"""
|
|
1411
|
+
paths = []
|
|
1412
|
+
for token in argv:
|
|
1413
|
+
if len(paths) >= FILE_OPERANDS_PER_SEGMENT:
|
|
1414
|
+
break
|
|
1415
|
+
path = resolve_operand(token)
|
|
1416
|
+
if path is None:
|
|
1417
|
+
continue
|
|
1418
|
+
try:
|
|
1419
|
+
key = os.path.realpath(path)
|
|
1420
|
+
except OSError:
|
|
1421
|
+
key = path
|
|
1422
|
+
if key in inspected_files:
|
|
1423
|
+
continue
|
|
1424
|
+
inspected_files.add(key)
|
|
1425
|
+
paths.append(path)
|
|
1426
|
+
return paths
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def file_creation(text, depth):
|
|
1430
|
+
"""An undeclared tracker creation inside a file's contents, or None.
|
|
1431
|
+
|
|
1432
|
+
Two recognisers, because a creation inside a file is not always shell.
|
|
1433
|
+
The shell path handles `bash create.sh`; the CONJUNCTION path handles
|
|
1434
|
+
`node wrapper.mjs`, a Python client, or anything else that speaks HTTP
|
|
1435
|
+
directly — it needs a tracker endpoint AND a creation verb in the same
|
|
1436
|
+
file, which is what keeps a changelog that merely mentions `issueCreate`
|
|
1437
|
+
from reading as a creation.
|
|
1438
|
+
|
|
1439
|
+
Args:
|
|
1440
|
+
text: The file's contents.
|
|
1441
|
+
depth: Current nesting depth.
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
A (signature, roles, cross_repo_target) triple, or None.
|
|
1445
|
+
"""
|
|
1446
|
+
nested = scan(text, depth + 1, from_file=True)
|
|
1447
|
+
if nested is not None:
|
|
1448
|
+
return nested
|
|
1449
|
+
# The coarse path answers to a coarse declaration check, and the precise
|
|
1450
|
+
# path above answers to the precise one. Reversing that — screening the
|
|
1451
|
+
# whole file first — would let a declaration on one create in a script
|
|
1452
|
+
# vouch for a different, undeclared one further down.
|
|
1453
|
+
if (
|
|
1454
|
+
GRAPHQL_CREATE.search(text)
|
|
1455
|
+
and TRACKER_ENDPOINT.search(text)
|
|
1456
|
+
and not text_declares_readiness(text)
|
|
1457
|
+
):
|
|
1458
|
+
return "a tracker creation", [ready_role], None
|
|
1459
|
+
return None
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def scan(text, depth, from_file=False):
|
|
1020
1463
|
"""Find the first undeclared tracker creation in a command string.
|
|
1021
1464
|
|
|
1022
1465
|
Args:
|
|
1023
1466
|
text: A shell command.
|
|
1024
1467
|
depth: Current nesting depth.
|
|
1468
|
+
from_file: Whether `text` is a file's contents rather than a typed
|
|
1469
|
+
command.
|
|
1025
1470
|
|
|
1026
1471
|
Returns:
|
|
1027
1472
|
A (signature, roles, cross_repo_target) triple, or None when nothing
|
|
@@ -1035,6 +1480,13 @@ def scan(text, depth):
|
|
|
1035
1480
|
# comment to bash, which strips it and RUNS the create, while shlex
|
|
1036
1481
|
# raises on the unbalanced quote. Two appended characters, no binary
|
|
1037
1482
|
# required. "I could not parse it" must never mean "it is fine".
|
|
1483
|
+
#
|
|
1484
|
+
# A FILE that does not lex is judged by the same recogniser rather than
|
|
1485
|
+
# waved through: an unbalanced quote inside a script is the identical
|
|
1486
|
+
# two-character trick moved one file away, and skipping it would hand
|
|
1487
|
+
# the bypass straight back.
|
|
1488
|
+
if from_file and text_declares_readiness(text):
|
|
1489
|
+
return None
|
|
1038
1490
|
if UNPARSEABLE_CREATION.search(text):
|
|
1039
1491
|
return (
|
|
1040
1492
|
"an unparseable command that reads as a tracker creation",
|
|
@@ -1049,7 +1501,8 @@ def scan(text, depth):
|
|
|
1049
1501
|
if name not in TRACKER_CLIS and name not in HTTP_CLIS:
|
|
1050
1502
|
continue
|
|
1051
1503
|
args = argv[index + 1 :]
|
|
1052
|
-
|
|
1504
|
+
submitted = payload_text(args, text)
|
|
1505
|
+
signature = creation_signature(name, args, submitted)
|
|
1053
1506
|
if signature is None:
|
|
1054
1507
|
continue
|
|
1055
1508
|
# The ambient override is the human operator's. An inline
|
|
@@ -1058,7 +1511,18 @@ def scan(text, depth):
|
|
|
1058
1511
|
if ambient_override and not inline_override:
|
|
1059
1512
|
continue
|
|
1060
1513
|
roles, target = roles_for(target_repository(args))
|
|
1061
|
-
|
|
1514
|
+
state_role_ok = tracker in STATE_ROLE_TRACKERS and target is None
|
|
1515
|
+
if declares_readiness(args, roles, submitted, state_role_ok):
|
|
1516
|
+
continue
|
|
1517
|
+
# `LIFECYCLE_ROLE=ready curl …` puts the declaration BEFORE the
|
|
1518
|
+
# client, which is where an inline assignment has to go, so the
|
|
1519
|
+
# role is read from the whole segment rather than from the client's
|
|
1520
|
+
# own arguments. Scoped to the segment and not the command, so a
|
|
1521
|
+
# declaration cannot be shouted from an unrelated pipeline stage.
|
|
1522
|
+
if state_role_ok and LIFECYCLE_ROLE_READY.search(" ".join(argv)):
|
|
1523
|
+
continue
|
|
1524
|
+
# A script declares once, for itself. See `scope_declaration`.
|
|
1525
|
+
if from_file and scope_declaration(text, state_role_ok):
|
|
1062
1526
|
continue
|
|
1063
1527
|
return signature, roles, target
|
|
1064
1528
|
|
|
@@ -1077,6 +1541,36 @@ def scan(text, depth):
|
|
|
1077
1541
|
nested = scan(operand, depth + 1)
|
|
1078
1542
|
if nested is not None:
|
|
1079
1543
|
return nested
|
|
1544
|
+
|
|
1545
|
+
# The locate step, which used to stop at `bash` and never reach the
|
|
1546
|
+
# operand. Deliberately last: an inline creation is the cheaper and
|
|
1547
|
+
# more precise finding, so it is reported before a file is opened.
|
|
1548
|
+
# The operator's ambient override is checked BEFORE the depth bound,
|
|
1549
|
+
# not after it. The other order meant a creation reached past the cap
|
|
1550
|
+
# was refused even with `LISA_ALLOW_DIRECT_ISSUE_CREATE=1` exported —
|
|
1551
|
+
# while the refusal text advertised that escape. An escape hatch the
|
|
1552
|
+
# refusal names and the code ignores is worse than none. Caught by
|
|
1553
|
+
# review before it shipped.
|
|
1554
|
+
if ambient_override and not inline_override:
|
|
1555
|
+
continue
|
|
1556
|
+
for path in file_operands(argv):
|
|
1557
|
+
contents = read_operand(path)
|
|
1558
|
+
if not contents:
|
|
1559
|
+
continue
|
|
1560
|
+
if depth >= MAX_NESTING_DEPTH:
|
|
1561
|
+
if GRAPHQL_CREATE.search(contents) or UNPARSEABLE_CREATION.search(
|
|
1562
|
+
contents
|
|
1563
|
+
):
|
|
1564
|
+
return (
|
|
1565
|
+
"a tracker creation inside %s, nested past the "
|
|
1566
|
+
"inspection depth" % path,
|
|
1567
|
+
[ready_role],
|
|
1568
|
+
None,
|
|
1569
|
+
)
|
|
1570
|
+
continue
|
|
1571
|
+
found = file_creation(contents, depth)
|
|
1572
|
+
if found is not None:
|
|
1573
|
+
return ("%s inside %s" % (found[0], path), found[1], found[2])
|
|
1080
1574
|
return None
|
|
1081
1575
|
|
|
1082
1576
|
|
|
@@ -1115,6 +1609,8 @@ verdict="$(
|
|
|
1115
1609
|
LISA_GUARD_UPSTREAM_REPO="$upstream_repo" \
|
|
1116
1610
|
LISA_GUARD_UPSTREAM_READY_ROLE="$upstream_ready_role" \
|
|
1117
1611
|
LISA_GUARD_CALLER_IS_GITHUB="$caller_is_github" \
|
|
1612
|
+
LISA_GUARD_TRACKER="$tracker" \
|
|
1613
|
+
LISA_GUARD_PROJECT_DIR="$project_dir" \
|
|
1118
1614
|
python3 -
|
|
1119
1615
|
)"
|
|
1120
1616
|
python_status=$?
|