@codyswann/lisa 4.52.1 → 4.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +12 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- 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-repair-intake/SKILL.md +23 -4
- package/plugins/lisa/rules/reference/blocker-containment.md +2 -0
- package/plugins/lisa/scripts/blocker-edge-resolution.mjs +150 -0
- package/plugins/lisa/scripts/install-remote-agent-aws.mjs +14 -0
- package/plugins/lisa/scripts/remote-agent-aws-setup.sh +134 -0
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +23 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/blocker-edge-resolution.mjs +150 -0
- package/plugins/lisa-agy/scripts/install-remote-agent-aws.mjs +14 -0
- package/plugins/lisa-agy/scripts/remote-agent-aws-setup.sh +134 -0
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +23 -4
- 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/rules/reference/blocker-containment.md +2 -0
- package/plugins/lisa-copilot/scripts/blocker-edge-resolution.mjs +150 -0
- package/plugins/lisa-copilot/scripts/install-remote-agent-aws.mjs +14 -0
- package/plugins/lisa-copilot/scripts/remote-agent-aws-setup.sh +134 -0
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +23 -4
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +2 -0
- package/plugins/lisa-cursor/scripts/blocker-edge-resolution.mjs +150 -0
- package/plugins/lisa-cursor/scripts/install-remote-agent-aws.mjs +14 -0
- package/plugins/lisa-cursor/scripts/remote-agent-aws-setup.sh +134 -0
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +23 -4
- 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/rules/reference/blocker-containment.md +2 -0
- package/plugins/src/base/scripts/blocker-edge-resolution.mjs +150 -0
- package/plugins/src/base/scripts/install-remote-agent-aws.mjs +14 -0
- package/plugins/src/base/scripts/remote-agent-aws-setup.sh +134 -0
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +23 -4
|
@@ -160,4 +160,6 @@ An `is blocked by` link is satisfied once the code you need is on your base bran
|
|
|
160
160
|
|
|
161
161
|
It does not decide **when** a dependency edge should exist, dissolve an edge once satisfied, or govern what a surface does after clearing — re-dispatch, re-validation, and lifecycle transitions remain the consuming skill's business. It governs exactly one question: given an edge, is it satisfied.
|
|
162
162
|
|
|
163
|
+
**What follows from a verdict now has an owner**, because for a while nothing did and the gap was live: a repair pass ruled a dependency satisfied, moved the state, and left the edge, recording the ruling as a comment — which the next reader never sees, because it parses relations rather than prose. The block re-formed on the next cycle (CodySwannGT/lisa#3472). Measured on this repository: of 11 open `status:blocked` items carrying a parsable `Blocked by #N`, 5 were held behind blockers that were all closed. Feed this rule's verdict to `scripts/blocker-edge-resolution.mjs`, which returns `dissolve` / `keep` / `escalate`; it consumes the containment verdict and never re-derives it.
|
|
164
|
+
|
|
163
165
|
It also carries no memory of its own prior verdicts. A surface that re-runs this predicate after a human reversed a transition it produced will reach the same answer from the same facts; making that reversal durable is a separate concern belonging to the consuming skill's loop-prevention contract, not to this predicate.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* What to do with a dependency edge once its blocker has been judged.
|
|
4
|
+
*
|
|
5
|
+
* ## The gap this fills
|
|
6
|
+
*
|
|
7
|
+
* `blocker-containment` decides ONE question — given an edge, is it satisfied —
|
|
8
|
+
* and says so in its own words: it "does not decide when a dependency edge
|
|
9
|
+
* should exist, dissolve an edge once satisfied, or govern what a surface does
|
|
10
|
+
* after clearing". Nothing else claimed that job. So a repair pass that ruled a
|
|
11
|
+
* dependency satisfied moved the STATE and left the EDGE, recording the ruling
|
|
12
|
+
* as a comment; the next reader parses relations, not prose, and blocks again
|
|
13
|
+
* (CodySwannGT/lisa#3472). The durable artifact of "this is satisfied" had no
|
|
14
|
+
* mechanical effect.
|
|
15
|
+
*
|
|
16
|
+
* Measured on this repository while writing this: of 11 open `status:blocked`
|
|
17
|
+
* items carrying a parsable `Blocked by #N`, 5 were held behind blockers that
|
|
18
|
+
* were all closed.
|
|
19
|
+
*
|
|
20
|
+
* ## Why closure is not the test, and neither is "it is finished"
|
|
21
|
+
*
|
|
22
|
+
* Two different mistakes converge on the same wrong answer.
|
|
23
|
+
*
|
|
24
|
+
* The first is testing a state NAME. That half is already fixed: the triage
|
|
25
|
+
* gate now clear-checks by containment, because a promotion workflow types its
|
|
26
|
+
* merged-and-deployed rungs `started`, so `Done` strands every dependent behind
|
|
27
|
+
* work already merged where the dependent needs it.
|
|
28
|
+
*
|
|
29
|
+
* The second is treating CLOSED as satisfied. An `is blocked by` edge is a
|
|
30
|
+
* DEVELOPMENT dependency: it is satisfied when the blocker's code is on the
|
|
31
|
+
* branch this work builds from. A blocker can be closed with no code — and a
|
|
32
|
+
* blocker closed `NOT_PLANNED` is the sharp case, because it means the thing
|
|
33
|
+
* depended on will never be built. Dissolving that edge silently converts "what
|
|
34
|
+
* we needed is cancelled" into "we are unblocked", which is the same wrong-write
|
|
35
|
+
* family as CodySwannGT/lisa#3479, where a reason-blind repair would have
|
|
36
|
+
* stamped a production terminal on abandoned work. So an abandoned blocker
|
|
37
|
+
* ESCALATES to a human rather than dissolving or silently holding forever:
|
|
38
|
+
* somebody has to decide whether the dependent still makes sense.
|
|
39
|
+
*
|
|
40
|
+
* ## What this module does not decide
|
|
41
|
+
*
|
|
42
|
+
* How an edge is dissolved on a given tracker. That is vendor work with its own
|
|
43
|
+
* hazard: on Linear, `issueRelationCreate` CONVERTS an existing relation rather
|
|
44
|
+
* than adding one (CodySwannGT/lisa#3605), so a writer that "updates" an edge
|
|
45
|
+
* can destroy a different one. This module returns a decision; the vendor
|
|
46
|
+
* surface performs it and reads it back.
|
|
47
|
+
* @module blocker-edge-resolution
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** Every action a surface may take on a judged edge. */
|
|
51
|
+
export const EDGE_ACTIONS = Object.freeze(["dissolve", "keep", "escalate"]);
|
|
52
|
+
|
|
53
|
+
/** GitHub's closure reason for work that will deliberately not be done. */
|
|
54
|
+
const NOT_PLANNED = "not_planned";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decide what happens to one `is blocked by` edge.
|
|
58
|
+
*
|
|
59
|
+
* `contained` is the containment verdict — the blocker's merged code is an
|
|
60
|
+
* ancestor of the branch this work builds from — and it is deliberately the
|
|
61
|
+
* ONLY positive signal. Everything else here exists to keep a non-containment
|
|
62
|
+
* signal from being mistaken for one.
|
|
63
|
+
* @param {{
|
|
64
|
+
* contained?: boolean | null,
|
|
65
|
+
* blockerState?: string,
|
|
66
|
+
* blockerReason?: string,
|
|
67
|
+
* }} input The containment verdict and the blocker's tracker state.
|
|
68
|
+
* @returns {{action: string, reason: string}} What to do, and why.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveBlockerEdge(input = {}) {
|
|
71
|
+
const state = String(input.blockerState ?? "")
|
|
72
|
+
.trim()
|
|
73
|
+
.toLowerCase();
|
|
74
|
+
const reason = String(input.blockerReason ?? "")
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase();
|
|
77
|
+
|
|
78
|
+
// Checked BEFORE containment, because an abandoned blocker whose branch
|
|
79
|
+
// happens to contain some earlier commit of its own is still abandoned. The
|
|
80
|
+
// question a human has to answer is whether the dependent still makes sense,
|
|
81
|
+
// and a containment true would silently answer it for them.
|
|
82
|
+
if (state === "closed" && reason === NOT_PLANNED) {
|
|
83
|
+
return {
|
|
84
|
+
action: "escalate",
|
|
85
|
+
reason:
|
|
86
|
+
"the blocker was closed as NOT PLANNED, so the work this item depends on will never be " +
|
|
87
|
+
"delivered. Dissolving the edge would read as 'unblocked' when nothing was built, and " +
|
|
88
|
+
"holding it forever hides a decision somebody has to make: does this item still make sense " +
|
|
89
|
+
"without its blocker? That is a human call, not a sweep's.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (input.contained === true) {
|
|
94
|
+
return {
|
|
95
|
+
action: "dissolve",
|
|
96
|
+
reason:
|
|
97
|
+
"the blocker's merged code is present on the branch this item builds from, so the " +
|
|
98
|
+
"development dependency is satisfied. Remove the edge in the same write that records the " +
|
|
99
|
+
"ruling, and read it back — a state move plus a comment is a claim about a clearance, not " +
|
|
100
|
+
"a clearance, and the next reader parses relations rather than prose.",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (input.contained === null || input.contained === undefined) {
|
|
105
|
+
return {
|
|
106
|
+
action: "escalate",
|
|
107
|
+
reason:
|
|
108
|
+
"containment could not be computed for this blocker, so whether the dependency is satisfied " +
|
|
109
|
+
"is unknown. An unknown is not a clearance: dissolving here would be a guess in the " +
|
|
110
|
+
"direction that releases work, which is the direction nothing downstream re-checks.",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
action: "keep",
|
|
116
|
+
reason:
|
|
117
|
+
state === "closed"
|
|
118
|
+
? "the blocker is closed but its code is not on the branch this item builds from. Closing an " +
|
|
119
|
+
"item is not the same as shipping it, and an `is blocked by` edge is a development " +
|
|
120
|
+
"dependency, not a bookkeeping one."
|
|
121
|
+
: "the blocker's code is not yet on the branch this item builds from, so the dependency " +
|
|
122
|
+
"still holds.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Decide a whole set of edges, and say whether the item can proceed.
|
|
128
|
+
*
|
|
129
|
+
* An item proceeds only when every edge dissolves. One escalation holds the
|
|
130
|
+
* item — reporting it, rather than proceeding on the majority verdict, is what
|
|
131
|
+
* keeps a single unanswerable edge from being rounded away.
|
|
132
|
+
* @param {readonly object[]} edges Per-edge inputs, in the item's own order.
|
|
133
|
+
* @returns {{
|
|
134
|
+
* proceed: boolean,
|
|
135
|
+
* decisions: {action: string, reason: string}[],
|
|
136
|
+
* escalations: number,
|
|
137
|
+
* }} The per-edge decisions and whether the item is free to move.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveBlockerEdges(edges = []) {
|
|
140
|
+
const decisions = (edges ?? []).map(edge => ({
|
|
141
|
+
...edge,
|
|
142
|
+
...resolveBlockerEdge(edge),
|
|
143
|
+
}));
|
|
144
|
+
return {
|
|
145
|
+
proceed:
|
|
146
|
+
decisions.length > 0 && decisions.every(one => one.action === "dissolve"),
|
|
147
|
+
decisions,
|
|
148
|
+
escalations: decisions.filter(one => one.action === "escalate").length,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -342,6 +342,20 @@ so rather than repointing every unqualified \`aws\` command. Pass
|
|
|
342
342
|
Production and shared profiles are observer-only. Production repair remains a
|
|
343
343
|
human-driven local-workstation operation.
|
|
344
344
|
|
|
345
|
+
Observer-only names what those profiles may READ, not only what they may not
|
|
346
|
+
write. Prove that half — a role that can read nothing satisfies every deny-side
|
|
347
|
+
assertion ever made about it:
|
|
348
|
+
|
|
349
|
+
\`\`\`bash
|
|
350
|
+
LISA_AWS_VERIFY_OBSERVER_READS=1 bash scripts/remote-agent-aws-setup.sh
|
|
351
|
+
\`\`\`
|
|
352
|
+
|
|
353
|
+
It runs one representative read per observability surface against every observer
|
|
354
|
+
profile — deployment stacks, compute inventory, alarm state, delivery pipelines,
|
|
355
|
+
builds, functions, logs, http endpoints, queues, workflows — and refuses to
|
|
356
|
+
report ready if any is denied, naming all of them at once. Every action is
|
|
357
|
+
read-only, so widening the role's policy to satisfy it grants no write anywhere.
|
|
358
|
+
|
|
345
359
|
After the infrastructure pipeline deploys the remote-agent IAM stacks, retrieve
|
|
346
360
|
the complete bundle from the shared account. Do not extract or distribute its
|
|
347
361
|
individual access-key fields:
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
# LISA_AWS_VERIFY_ALL_PROFILES
|
|
28
28
|
# 1 to prove every stage's account, not just the
|
|
29
29
|
# default's.
|
|
30
|
+
# LISA_AWS_VERIFY_OBSERVER_READS
|
|
31
|
+
# 1 to prove the ALLOW side of every observer-only
|
|
32
|
+
# profile: one representative read per observability
|
|
33
|
+
# surface must succeed. A role that can read nothing
|
|
34
|
+
# fails here instead of at verification time.
|
|
30
35
|
# LISA_AWS_CLAIM_DEFAULT_PROFILE
|
|
31
36
|
# 1 to take over a `[default]` this project does not
|
|
32
37
|
# already own. Off by default: silently repointing
|
|
@@ -337,6 +342,120 @@ assert_profile_account() {
|
|
|
337
342
|
"profile ${profile} (stage ${stage}) resolved to AWS account ${actual}, but this project expects ${expected}. The credentials authenticate; they are not this project's account. Refusing to report ready."
|
|
338
343
|
}
|
|
339
344
|
|
|
345
|
+
# What an observer-only role must be able to READ.
|
|
346
|
+
#
|
|
347
|
+
# "Observer-only" was defined entirely by what it must not do, and the proof
|
|
348
|
+
# prescribed for it tested only the deny side. A role that can read NOTHING
|
|
349
|
+
# passes a deny-side check perfectly, so the read surface of a headless verifier
|
|
350
|
+
# was being discovered one `AccessDenied` at a time, mid-verification, and
|
|
351
|
+
# granted one ticket at a time. Three consecutive incidents in one consumer had
|
|
352
|
+
# that exact shape: instance discovery, alarm reads, and pipeline definition
|
|
353
|
+
# reads — none of those services appeared in the shipped starter policy at all,
|
|
354
|
+
# because that policy enumerated the services the stack happened to deploy
|
|
355
|
+
# rather than answering "what must a verifier be able to read?".
|
|
356
|
+
#
|
|
357
|
+
# So the surfaces below are derived from the ROLE'S PURPOSE, not from any one
|
|
358
|
+
# stack's topology. Each row is a surface, the read that represents it, and the
|
|
359
|
+
# question an observer cannot answer without it. Every action is read-only:
|
|
360
|
+
# widening the allow side generously is far cheaper than rediscovering it by
|
|
361
|
+
# denial, and it grants no write anywhere.
|
|
362
|
+
#
|
|
363
|
+
# Each probe takes no resource name, which is what lets it run against a fresh
|
|
364
|
+
# account with nothing deployed — these ask an IAM question, not an inventory
|
|
365
|
+
# one, and an empty result is a pass. The limitation that buys: a surface whose
|
|
366
|
+
# list action is granted while its detail action is not still passes here. That
|
|
367
|
+
# is exactly incident three, where the role could list pipeline executions but
|
|
368
|
+
# could not read the pipeline definition, so the table names both halves in the
|
|
369
|
+
# question column and the docs state the requirement; the probe proves the
|
|
370
|
+
# surface is reachable, not that every action on it is granted.
|
|
371
|
+
OBSERVER_READ_SURFACES=$(
|
|
372
|
+
cat <<'SURFACES'
|
|
373
|
+
deployment stacks cloudformation describe-stacks what is deployed, and did the last change apply or roll back
|
|
374
|
+
compute inventory ec2 describe-instances what is actually running, and is it healthy
|
|
375
|
+
alarm state cloudwatch describe-alarms is anything alarming right now, and since when
|
|
376
|
+
delivery pipelines codepipeline list-pipelines what the pipeline does and where this change stopped, definition as well as executions
|
|
377
|
+
builds codebuild list-projects did the build run, and what did it say
|
|
378
|
+
functions lambda list-functions is the handler deployed, and at which version
|
|
379
|
+
logs logs describe-log-groups what it printed when it failed
|
|
380
|
+
http endpoints apigateway get-rest-apis is the endpoint published and reachable
|
|
381
|
+
queues sqs list-queues is work backing up or dead-lettering
|
|
382
|
+
workflows stepfunctions list-state-machines did the orchestrated run complete or stall
|
|
383
|
+
SURFACES
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Stages whose profile is observer-only, per the contract's own words.
|
|
387
|
+
#
|
|
388
|
+
# The bootstrap bundle carries no observer field today, so this reads one if a
|
|
389
|
+
# bundle supplies it and otherwise falls back to the definition the contract
|
|
390
|
+
# already states in prose: dev and staging may repair, production and shared are
|
|
391
|
+
# observer-only. Deriving it is what makes "observer" exist anywhere other than
|
|
392
|
+
# a sentence in a document — until now nothing in the data or the code could
|
|
393
|
+
# answer which profiles the word applied to.
|
|
394
|
+
is_observer_stage() {
|
|
395
|
+
local stage declared
|
|
396
|
+
stage="$1"
|
|
397
|
+
# `has` rather than `.observer // empty`: jq's alternative operator treats
|
|
398
|
+
# `false` as absent, so a stage that explicitly declared itself NOT an
|
|
399
|
+
# observer fell through to the name-based default and was probed anyway —
|
|
400
|
+
# the declaration was accepted only when it agreed with the guess.
|
|
401
|
+
declared="$(printf '%s' "$profiles_json" | jq -r --arg s "$stage" '
|
|
402
|
+
.[$s] | if type == "object" and has("observer") then (.observer | tostring) else empty end
|
|
403
|
+
')"
|
|
404
|
+
if [ -n "$declared" ]; then
|
|
405
|
+
[ "$declared" = "true" ]
|
|
406
|
+
return
|
|
407
|
+
fi
|
|
408
|
+
case "$stage" in
|
|
409
|
+
production | shared) return 0 ;;
|
|
410
|
+
*) return 1 ;;
|
|
411
|
+
esac
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Prove the ALLOW side of an observer profile.
|
|
415
|
+
#
|
|
416
|
+
# Reports every surface it could not read rather than stopping at the first,
|
|
417
|
+
# because an operator repairing a policy wants the whole list — the same reason
|
|
418
|
+
# the toolchain planner reports every unresolvable tool at once. One ticket, not
|
|
419
|
+
# three in a row.
|
|
420
|
+
#
|
|
421
|
+
# Denied and unreachable are kept apart. A read that fails because the service
|
|
422
|
+
# could not be reached says nothing about the policy, and folding it into
|
|
423
|
+
# "denied" would send someone to widen a policy that was already correct; a pass
|
|
424
|
+
# is never claimed from either.
|
|
425
|
+
assert_observer_reads() {
|
|
426
|
+
local profile stage denied unreachable surface probe question output status
|
|
427
|
+
profile="$1"
|
|
428
|
+
stage="$2"
|
|
429
|
+
denied=""
|
|
430
|
+
unreachable=""
|
|
431
|
+
|
|
432
|
+
while IFS="$(printf '\t')" read -r surface probe question; do
|
|
433
|
+
[ -n "$surface" ] || continue
|
|
434
|
+
set +e
|
|
435
|
+
output="$(AWS_PAGER="" aws --profile "$profile" $probe 2>&1)"
|
|
436
|
+
status=$?
|
|
437
|
+
set -e
|
|
438
|
+
[ "$status" -eq 0 ] && continue
|
|
439
|
+
case "$output" in
|
|
440
|
+
*AccessDenied* | *UnauthorizedOperation* | *not\ authorized*)
|
|
441
|
+
denied="${denied}
|
|
442
|
+
${surface} — \`aws ${probe}\` was denied, so this role cannot answer: ${question}"
|
|
443
|
+
;;
|
|
444
|
+
*)
|
|
445
|
+
unreachable="${unreachable}
|
|
446
|
+
${surface} — \`aws ${probe}\` did not return an authorization answer at all: ${output}"
|
|
447
|
+
;;
|
|
448
|
+
esac
|
|
449
|
+
done <<<"$OBSERVER_READ_SURFACES"
|
|
450
|
+
|
|
451
|
+
if [ -n "$unreachable" ]; then
|
|
452
|
+
fail "profile ${profile} (stage ${stage}) could not be checked against the observer read surface, so nothing about its permissions was proved:${unreachable}"
|
|
453
|
+
fi
|
|
454
|
+
if [ -n "$denied" ]; then
|
|
455
|
+
fail "profile ${profile} (stage ${stage}) is observer-only but cannot read the surfaces an observer exists to read. Widen the role's read-only policy to cover them; every action below is read-only and none grants a write:${denied}"
|
|
456
|
+
fi
|
|
457
|
+
}
|
|
458
|
+
|
|
340
459
|
# Who currently owns `[default]`.
|
|
341
460
|
#
|
|
342
461
|
# `default` is the one name that cannot be namespaced, so it is the one place
|
|
@@ -530,6 +649,21 @@ if [ "${LISA_AWS_SKIP_VERIFY:-0}" != "1" ]; then
|
|
|
530
649
|
else
|
|
531
650
|
assert_profile_account "$default_profile" "$default_expected_account" "$default_stage"
|
|
532
651
|
fi
|
|
652
|
+
# Ordered after the account assertions on purpose, and it is the ordering that
|
|
653
|
+
# makes either half mean anything. `AccessDenied` is what an unassumed role, a
|
|
654
|
+
# misconfigured profile and a correctly-scoped observer all return, so a
|
|
655
|
+
# boundary check run before identity is bound passes on a credential that
|
|
656
|
+
# reached the wrong account entirely — the same class of vacuous pass this
|
|
657
|
+
# script already refuses for `sts:GetCallerIdentity`. Binding the profile to
|
|
658
|
+
# its expected account first is what turns the reads below into a statement
|
|
659
|
+
# about THIS role.
|
|
660
|
+
if [ "${LISA_AWS_VERIFY_OBSERVER_READS:-0}" = "1" ]; then
|
|
661
|
+
while IFS= read -r stage; do
|
|
662
|
+
[ -n "$stage" ] || continue
|
|
663
|
+
is_observer_stage "$stage" || continue
|
|
664
|
+
assert_observer_reads "${PROFILE_NAMESPACE}-agent-${stage}" "$stage"
|
|
665
|
+
done < <(printf '%s' "$profiles_json" | jq -r 'keys[]')
|
|
666
|
+
fi
|
|
533
667
|
fi
|
|
534
668
|
|
|
535
669
|
if [ -n "$legacy_names" ]; then
|
|
@@ -566,10 +566,29 @@ condition observed, filed under the wrong heading, and overridden.
|
|
|
566
566
|
**deployed / runtime verification failure**, by an **ambiguity**, or by more than one at once.
|
|
567
567
|
Re-check **every** class present — do not stop at "no `is blocked by` links, therefore nothing
|
|
568
568
|
to do." A self-block has zero dependencies by definition, yet is fully re-checkable.
|
|
569
|
-
2. **Dependency cleared** —
|
|
570
|
-
`
|
|
571
|
-
|
|
572
|
-
|
|
569
|
+
2. **Dependency cleared** — decide each parsed `is blocked by` edge with
|
|
570
|
+
`scripts/blocker-edge-resolution.mjs`, feeding it the `blocker-containment` verdict plus the
|
|
571
|
+
blocker's state and closure reason. It returns `dissolve` / `keep` / `escalate` per edge.
|
|
572
|
+
**Dissolve the edge in the SAME write that records the ruling, then read the relation back and
|
|
573
|
+
confirm it is gone.** Only when every edge dissolves → move `blocked → claimed`, then run the
|
|
574
|
+
same agent-dispatch + post-agent `claimed → done` sequence as the stalled-`claimed` path above
|
|
575
|
+
(one-cycle recovery). If the agent re-blocks, move back to `blocked` — a valid outcome.
|
|
576
|
+
|
|
577
|
+
**Moving the state without removing the edge is not a clearance, it is a claim about one.** That
|
|
578
|
+
was the defect (CodySwannGT/lisa#3472): the pass moved the state and left the relation, recording
|
|
579
|
+
the ruling as a comment, and the next reader parses relations rather than prose — so the block
|
|
580
|
+
re-formed every cycle and a human had to break the loop. Reading the relation back is what makes
|
|
581
|
+
"I removed the link" distinguishable from "I meant to".
|
|
582
|
+
|
|
583
|
+
**An `escalate` verdict holds the item and reports it — never dissolve on it.** The sharp case is
|
|
584
|
+
a blocker closed `NOT PLANNED`: the work depended on will never be delivered, so dissolving reads
|
|
585
|
+
as "unblocked" when nothing was built, and holding silently hides a decision only a human can
|
|
586
|
+
make. Treating closure as satisfaction is the same reason-blind wrong write as
|
|
587
|
+
CodySwannGT/lisa#3479. A single escalation among otherwise-dissolving edges holds the item.
|
|
588
|
+
|
|
589
|
+
**On Linear, dissolve with a relation DELETE.** Do not "update" an edge:
|
|
590
|
+
`issueRelationCreate` CONVERTS an existing relation rather than adding one
|
|
591
|
+
(CodySwannGT/lisa#3605), so a writer reaching for it can destroy a different edge.
|
|
573
592
|
3. **Validation / quality-gate self-block re-check** — if the block reason is a pre-flight
|
|
574
593
|
`verify`/`validate` gate failure (its `[lisa-*]` block note carries a gate marker + a "Missing
|
|
575
594
|
requirements" list and there is **no** open `is blocked by` dependency), re-run the **same gate**
|