Skip to content

feat: gate every user resource — compute, email, and search - #202

Merged
alongubkin merged 45 commits into
mainfrom
itamar/alien-330-gate-all-resources
Jul 26, 2026
Merged

feat: gate every user resource — compute, email, and search#202
alongubkin merged 45 commits into
mainfrom
itamar/alien-330-gate-all-resources

Conversation

@ItamarZand88

Copy link
Copy Markdown
Contributor

Summary

.enabled() now covers every user resource: compute gates as a live existence choice, email and search gate at setup — each admitted with zero type-specific gating code, which is what the generator-owned mechanism exists for. This PR also pins the semantics: a frozen gate is answered once and enforced for the deployment's lifetime, and a declined live workload keeps its derived baseline so acceptance can return.

What happens when a deployer changes a gate's value on an update:

  1. A value conflicting with a recorded frozen answer refuses the update before anything runs (FROZEN_GATE_ANSWER_CHANGED); a re-registration that flips one fails synchronously, so a CloudFormation parameter edit rolls the whole stack update back.
  2. Frozen declines strip before the mutations — a declined setup resource never existed, nothing derives from it — while live declines strip after them, so service accounts, profile grants, and capacity stay identical whether a live workload is on or off. ← the heart
  3. The executor's ordinary create/delete planning applies the answer: declining deletes the resource (data included), accepting recreates it, exactly like removing or re-adding it from a release.

This PR changes the gateable set from four store types to every user resource, with frozen answers fixed and live answers reversible.

What was broken

  • The two deployment paths disagreed on ordering: the initial path derived infrastructure before stripping declines, the update path after — so declining a live workload on an update would have rederived its service account away and tripped the frozen-compatibility check.
  • A frozen gate on a direct deployment (no setup template) ignored the input entirely and created the resource regardless of the deployer's answer.
  • Nothing recorded a frozen gate's answer, so nothing could refuse a later change — it was silently ignored rather than an error.

What I did

  • Widened the gateability policy: worker, daemon, and container gate live; email and the experimental search collection gate frozen. The matrices grew fixtures for each, a live-only arm proving a gated live worker is invisible to setup while the deployer is still asked for the input, and a proof that the SES inbound grant follows Email's gate through both deploy-time answers. Builders for all five expose .enabled(), held to the policy by the generated manifest.
  • Split the decline strip around the mutations on both paths, resolving frozen declines from the import when one exists (absence is the answer) and from the initial input values on direct deployments — never a guess.
  • Recorded the canonical frozen answers on the deployment's runtime metadata at creation (runner and import paths both), refused conflicting updates and re-registrations, and derived answers from settled state for deployments that predate the recording. Deliberately no protocol version bump: the field is additive, an older actor's strip already resolves frozen presence from state so it cannot flip a resource, and a bump would refuse every customer-scheduled pull agent the moment a newer manager writes state.
  • A gated worker consuming an ungated queue passes preflight — pausing the consumer is the point, and the queue's retention policy owns the backlog. The reverse direction stays refused.
  • Live-gate transitions log a structured audit event with an operation id derived from resource, input, answer, and release — identical across retries, fresh for a later flip, no stored counter.
  • Extended the enabled-demo distribution app with a gated worker pair on dedicated permission profiles: the accepted worker's function must exist, the declined worker's function must never be created, and both profiles' service accounts must exist — the persisted baseline.

Files touched

  • crates/alien-core/src/gateability.rs, deployment/state.rs — policy widening; GateAnswers on the runtime metadata
  • crates/alien-deployment/src/{pending,updating,lib,error}.rs — the split strip, answer resolution and enforcement, audit events, typed refusals
  • crates/alien-manager/src/routes/stack.rs, tests/stack_import.rs — import-time recording and the synchronous re-import refusal
  • crates/alien-preflights/src/compile_time/resource_enabled_valid.rs — compute ban removed; pause-consumer tests both directions
  • packages/core/src/{worker,daemon,container,email}.ts, experimental/aws-opensearch.ts, tests, manifest — the SDK surface
  • crates/alien-deployment/tests/test_platform.rs — worker decline/re-accept baseline test; frozen-answer lifetime test
  • tests/e2e/test-apps/enabled-demo/*, crates/alien-test/{src,tests}/distribution.rs — the gated worker pair and its cloud assertions

How I tested

  • State machine: cargo test -p alien-deployment --test test_platform drives the full cycles — a live worker declined and re-accepted across updates with its service account asserted present in the prepared stack throughout, and a frozen answer recorded, refused on conflict, accepted on match, and still refused after wiping the recorded answers to simulate an older deployment.
  • Import: cargo test -p alien-manager --test stack_import — a fresh import records the declined and accepted answers; a re-registration that flips one gets a synchronous 400 with FROZEN_GATE_ANSWER_CHANGED.
  • Suites: alien-core, alien-preflights, alien-terraform, alien-cloudformation, alien-deployment, alien-manager all green; SDK vitest 71 plus clean tsc.
  • Real cloud: three local runs of terraform_aws_push_enabled_demo each failed during image upload with an ngrok gateway error (Service Unavailable, before Terraform ran — no gating code in the path, no cloud resources created), so the run is dispatched on this branch in the cloud e2e workflow with the enabled-demo app filter; the PR should merge on that run's green.

I also ran a security review on the diff. What it checked:

  • A changed input creating or deleting a frozen resource — two independent layers: the explicit refusal, and the strip resolving from state presence rather than the new value; the lifetime test proves both directions.
  • An answer flip riding an authorized setup update — the re-registration check runs before setup-update authorization is minted.
  • A declined live workload leaving reachable access — its resource-scoped grants leave with it; what persists is the provisioning baseline (service account, provision grants) that re-acceptance requires, which the e2e asserts explicitly.
  • Mixed-version actors — an older actor ignores a flipped value (state-derived strip, no resource churn) instead of applying it; answers dropped by an old write-back are rebuilt from state presence.
    Nothing turned up.

Out of scope

  • Container decline coverage through the local harness, and scale-to-zero for capacity groups whose residents are all declined — capacity keeps its declared floor here.
  • Frozen postgres gating — postgres has no setup emitters yet; its gate stays live-only until it does.

The rules deciding which resources may carry .enabled() lived only in
the compile-time check, so a caller that renders without preflights was
guarded by each emitter's supports_enabled_when() opt-in alone. Moving
the type and id rules into alien-core lets the setup generators enforce
the same refusals at render time, which the generic gating post-pass
needs before that opt-in can be deleted.

The schema exporter now also writes a gateability manifest so the
TypeScript SDK's builder surface can be asserted against the same table
instead of drifting independently.
A gated resource's fragment can contain blocks the resource does not
own — GCP custom roles and Azure role definitions are stack-wide, and
the Azure ones land in whichever fragment the generator hands over
first. The gating post-pass needs a signal that survives dedup-driven
reordering, so fragments now record shared blocks by address instead of
each call site special-casing block types.
Gating a resource previously meant its emitter threading the gate
through every block and self-reference by hand, which is why only
converted emitters could render .enabled(). The post-pass produces the
same output from plain blocks: gate every owned block, rewrite
references to gated addresses (AST traversals and parsed string
templates) to count-indexed form, and refuse what cannot compose —
pre-existing count, for_each, or a side-effecting block inside a
footprintless residual.

References hidden in raw strings the AST walk cannot see are caught by
a rendered-output scan that fails generation on any unindexed gated
address outside depends_on, where Terraform requires whole-resource
references.
The generator now gates a fragment right after its emitter returns and
rewrites gated references module-wide once every fragment exists, with
the rendered-output scan as the final net. The per-type render refusal
is replaced by the shared gateability policy, so render-without-
preflights refuses exactly what the compile-time check refuses.
kv, queue, and storage across AWS, GCP, and Azure now emit plain,
unconditional blocks; the generator's post-pass produces the identical
gated render, proven by the unchanged gated snapshots. The one
deliberate survivor is the GCP service-account emitter's cross-gate
call: its project-wide grants follow the *target* resource's gate,
which is contribution knowledge only that emitter has.

supports_enabled_when() overrides are gone from the stripped emitters;
the policy-refusal render test replaces the unconverted-emitter test,
since 'unconverted' is no longer a state an emitter can be in.
…pt-in

The CloudFormation generator already stamped the gate's Condition onto
every emitted resource; the emitters' whole conversion was the
supports_enabled_when() opt-in. That flag is now deleted from both
setup-format traits — the shared gateability policy, re-checked at
render time, is what refuses an ungateable resource.

The SES inbound-delivery grant in the storage bucket policy becomes a
gated contribution: it carries the Email contributor's gate (rendering
byte-identically while Email is ungated), so the moment Email becomes
gateable a declined Email takes its SES write access with it instead
of leaving it in a sibling's policy.
The matrix walks the registry: every (type, platform) pair is either
refused by the gateability policy or rendered gated from a fixture,
validated with terraform validate, and asserted to splice its
registration entry. A newly registered emitter for a gateable type
fails the matrix until a fixture exists, so no type can silently
become gateable.

Its first run caught exactly that hole: network, remote-stack-
management, kubernetes-cluster, service activations, and the Azure
default-* auxiliaries were held back only by the deleted emitter
opt-in, so the policy's stack-derived list now names them.

Vault is the first type gated purely by the post-pass — no vault
emitter ever carried gating code — locked by per-cloud snapshots. The
Azure snapshot shows the residual allowlist reproducing the deliberate
ungated random_id naming suffix.
The CloudFormation matrix mirrors the Terraform one — every registered
emitter is policy-refused or rendered gated, resolved through the
deploy-time model to prove a declined fixture leaves no registration
entry — plus a render-side refusal test for a gated Email, proving the
policy holds without preflights. The gated vault render is locked by a
snapshot; on AWS a granting-free vault contributes no template
resources at all, so its gated render is exactly the registration
splice.

The TypeScript surface test reads the generated gateability manifest
and asserts every policy-allowed type offers .enabled() and every
refused type does not, so the SDK and the Rust policy cannot drift.
From review:

- splats over a gated address stay unindexed — a splat is already
  list-aware, and inserting the count index would splat one instance
  and break when the gate is off
- the rendered-output scan accepts only real instance indexes (digits
  or [*]) after a gated address, so a key index cannot escape both nets
- the depends_on exemption requires the attribute form 'depends_on = ['
  — the word in prose no longer exempts whatever bracket follows
- template %{if}/%{for} directives and expression-valued object keys
  are rewritten recursively
- the scan's refusal is OperationNotSupported (non-retryable): the same
  stack renders the same escape every time, so retrying is a loop
- the CloudFormation parameter-collision message renders without the
  continuation-indentation space runs, with tests for both formats'
  collision guards
- registered_keys() no longer sits between require() and its doc
  comment; the SDK surface test covers the refused-type builders
  (Daemon, Container, Email) explicitly
The vitest transform accepted the extensionless import and the
two-argument Worker construction; tsc under nodenext does not.
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

Real-cloud verification: the enabled-demo e2e (four gated store pairs + the gated worker pair + baseline service accounts) runs at https://github.com/alienplatform/alien/actions/runs/30128396618 on these exact commits.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR extends resource gating across compute, email, and search while preserving frozen answers and reversible live-resource baselines.

  • Records canonical frozen-gate answers in deployment runtime metadata.
  • Splits frozen and live decline stripping around deployment mutations.
  • Enforces frozen-answer consistency during updates and stack re-registration.
  • Adds SDK surfaces, infrastructure gating, audit events, and deployment/import coverage.

Confidence Score: 5/5

The PR appears safe to merge within the scope of the previous review threads.

No blocking failure remains.

Important Files Changed

Filename Overview
crates/alien-deployment/src/pending.rs Implements frozen-answer resolution, legacy reconstruction, fixity enforcement, and lifecycle-aware decline stripping; the displayed follow-up issues are addressed without an eligible distinct failure.
crates/alien-deployment/src/updating.rs Applies frozen checks and stripping before mutations while handling live gates afterward; no blocking incomplete or unsafe fix remains within the previous-thread scope.
crates/alien-manager/src/routes/stack.rs Reconstructs and validates frozen answers during re-registration, including guarded handling when the settled release is unavailable.
crates/alien-core/src/deployment/state.rs Adds backward-compatible persisted frozen-gate answers to runtime metadata.
crates/alien-core/src/gateability.rs Expands lifecycle-aware gateability policy to the newly supported user resources.
crates/alien-terraform/src/gating.rs Adds infrastructure-level gate propagation used by generated Terraform resources and grants.

Reviews (14): Last reviewed commit: "docs(deployment): legacy acceptance stil..." | Re-trigger Greptile

Comment thread crates/alien-manager/src/routes/stack.rs Outdated
Comment thread crates/alien-deployment/src/pending.rs
Comment thread crates/alien-deployment/src/updating.rs Outdated
Comment thread crates/alien-manager/src/routes/stack.rs Outdated
Comment thread crates/alien-deployment/src/updating.rs Outdated
Comment thread crates/alien-deployment/src/updating.rs Outdated
Comment thread crates/alien-deployment/src/updating.rs
The gateability policy drops its compute arm — declining a live
workload rides the same removal path as deleting it from a release,
and its provisioning baseline persists so acceptance can return — and
empties the not-yet-generic list now that email and opensearch render
gated through the post-pass (proven by their new matrix fixtures; the
SES inbound grant demonstrably follows the Email gate, and a Live gate
is proven invisible to setup while its input still reaches the
deployer).

Worker, Daemon, Container, Email, and the experimental AwsOpenSearch
builders gain .enabled(); the regenerated manifest keeps the SDK
surface test honest about which lifecycles each type gates in.
The initial path stripped declined resources after the mutations while
the update path stripped before them, so declining a live resource on
an update rederived service accounts, profiles, and capacity from a
stack missing it — tripping the frozen-compatibility check that the
initial path's ordering never trips.

Both paths now agree: frozen declines apply before the mutations (a
declined setup resource never existed, so nothing may be derived from
it), and live declines apply after them, at the boundary where the
executor's desired set is built, so a declined workload's provisioning
baseline stays identical to the accepted render and acceptance can
return. Proven by the worker decline/reaccept state-machine test.

A frozen gate on a direct deployment now resolves the initial input
values (provided value, else declared default, never a guess); the
empty state previously carried no answer and the resource was created
regardless of the deployer's choice. The manager's import route
composes the two strips back to back — no mutations run between them
there.
The canonical resolved answer for every input gating a Frozen resource
is recorded on the runtime metadata when the deployment is created —
the same record/state/sync surface that already carries the prepared
stack, so no store or protocol shape changes. The update path refuses
input values that conflict with a recorded answer; states from before
this contract derive their answers from the settled stack state
(presence IS the original answer once setup completed) and upgrade
atomically before the check, refusing rather than guessing when
resources sharing one gate disagree.

Deployment protocol CURRENT moves to 2 (MIN stays 1): an actor unaware
of the fixity contract would step a deployment against the deployer's
recorded answer, so older actors refuse newer states instead.

Live-gate transitions now emit a structured audit event when detected,
with an operation id derived from resource, input, answer, and release
— identical across retries of the same step by construction, fresh for
a later flip — completed or failed by the executor's ordinary
per-resource status transitions.

The preflight gains the pause-consumer contract tests: a gated worker
consuming an ungated queue passes (the queue's retention policy governs
the backlog), while an ungated worker consuming a gated queue stays
refused.
The import route now resolves every frozen-gating input's answer before
the strip and records it on the created deployment, so imported
deployments carry their answers from birth instead of deriving them on
their first update.

A re-registration whose derived answers conflict with the recorded ones
is refused synchronously with FROZEN_GATE_ANSWER_CHANGED — and because
the caller is the setup artifact's custom resource mid-stack-update,
the failed response is what forces CloudFormation to roll the parameter
edit back, restoring whatever its conditionals just created or deleted.
The refusal runs before setup-update authorization is minted, so an
answer flip cannot ride an otherwise-authorized setup update; answers
for inputs a new release introduces are recorded, and recorded answers
are never overwritten.
An additive optional field does not bump the version. Re-derived from
first principles, the bump bought nothing — an older actor's strip
already resolves frozen presence from state, so a flipped input is
ignored rather than applied,
and answers dropped by an old write-back are rebuilt faithfully by the
derive-when-empty fallback — while costing a lot: pull agents upgrade
on the customer's schedule, and a version-2 state would hard-refuse
every older agent the moment a newer manager writes one.

New actors refuse conflicting answers loudly; older actors keep the
silent-ignore behavior they always had. Nothing can flip a frozen
resource either way.
Adds a distribution test app with a worker plus a matched on/off pair of
every gated resource type (KV, storage, queue, vault) behind
.enabled(input). The test answers the four *-on inputs true and the four
*-off inputs false, provisions to AWS via Terraform, then asserts each
enabled resource and its grant reach the imported stack_state and the
account while each declined resource is absent. Wires TestApp::EnabledDemo
through the harness (input_values -> tfvars) and adds the terraform-aws
matrix entry to e2e-cloud.yml behind an enabled-demo app filter.
The compute gate rides the live strip, so the real-cloud proof is the
pair pattern the app already uses: the accepted worker's function must
exist while the declined worker's function is never provisioned — and
both dedicated profiles' service accounts must exist, the persisted
baseline that lets a later acceptance recreate the function without a
setup change. Neither gated worker links anything, so no grant depends
on them.
A legacy re-registration could adopt flipped answers through its empty
recorded map; the settled state is the ground truth, so the import
route now derives the baseline from it before comparing, and the flip
is refused the same as everywhere else.

An input whose last frozen resource left the release stayed frozen
forever; fixity now scopes to inputs that still gate a setup-created
resource in the declared stack, so freeing the input frees its answer.

An update omitting a shared input resolved the live gate straight to
the declared default, deprovisioning an accepted workload; the live
strip now resolves provided value, then recorded answer, then default
— frozen dominance for the omitted case.
terraform validate cannot tell an accepted render from a declined one —
the gate variable is unknown at validate time, so count is unknown and
both branches type-check. The matrix now checks every rendered block: one
that reads the gated resource's indexed address must carry the gate
itself, or the declined apply resolves an index into an empty list. A
type that owns no blocks (a name-prefixed vault) is allowed, since
dropping its registration entry is its whole gate.

A second test holds the deployer input values output and secret inputs
together: that output publishes every answer in the clear, which is only
safe while secret inputs cannot reach the module at all.

Also sentence-joins the generated input descriptions, which ran label
and description together in variables.tf, drops past-tense narration and
a stray space run from two test doc comments, and reframes the
protocol-version note as the invariant that must hold.
@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-330-generator-owned-gating branch from 7b91f28 to 726a475 Compare July 25, 2026 00:36
@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-330-gate-all-resources branch from 2ceea07 to 2444066 Compare July 25, 2026 00:36
Comment thread crates/alien-manager/src/routes/stack.rs
Routing build() through the base class left Resource used only in return
positions, which the lint gate requires be imported as a type.
Comment thread crates/alien-deployment/src/updating.rs Outdated
Comment thread crates/alien-deployment/src/updating.rs
Comment thread crates/alien-manager/src/routes/stack.rs
The live strip consulted the recorded answer for any input that had one,
including an input a later release stopped gating a frozen resource on.
A recorded no then overrode the release's declared default and
deprovisioned a live resource the release actually enables.

Dominance now applies only while the input still gates a frozen resource
— which is what it means. Once freed, a live gate resolves like any
other: provided value, else the declared default.
The audit's value_source said 'persisted' whenever a recorded answer
existed for the input, but the resolver only consults one while the
input still gates a frozen resource. A freed gate carrying a stale
recorded answer therefore resolved from the declared default while the
log blamed the recorded one — on exactly the delete and create
transitions the audit exists to explain.

Also states the live strip's real resolution order on its doc comment,
and records what the legacy derivation gives up by refusing to infer a
decline from absence.
Comment thread crates/alien-deployment/src/pending.rs Outdated
Comment thread crates/alien-deployment/src/updating.rs Outdated
Resources gated on one input share a policy, and each contributes the
same permission set, so their statements arrive carrying the same id.
IAM rejects a policy document that repeats one, and two ordinary workers
with commands enabled are enough to hit it — the profile mutation
derives the dispatch set per resource independently.

Every other place that merges statements into one document already
normalizes the ids; this path was the only one that did not.
Comment thread crates/alien-deployment/src/updating.rs Outdated
Azure folds every resource-scoped management permission set into one
role assignment, so the grant belongs to all of its contributors at
once. It now carries the OR of their gates and disappears when the last
one is declined; a single ungated contributor keeps it unconditional,
because that resource still needs it.

The runtime built the same grant from the management profile alone, and
the profile is authored before the declined-resource strip and never
pruned by it — so it would have restored exactly what the template now
withholds. It now skips profile entries naming a resource that is no
longer in the desired stack, which is how AWS already reads a decline.

The role definition stays ungated: a definition nothing is assigned to
grants nothing. The limit is written down where the gate is applied —
these grants are resource-group scoped, so declining one worker while a
sibling stays enabled cannot revoke it.
A gated frozen resource was dropped when it was missing from the settled
state. That reads absence as a decline, and absence has two causes: the
deployer said no, or the release being deployed just introduced the gate.
Conflating them meant a release that adds a gated frozen resource had it
deleted from the prepared stack and the update reported success — while
the same release adding an UNGATED frozen resource got the clear refusal
telling the operator to rerun setup. Gating a resource turned an
actionable error into silence.

The strip now has one rule on every path: a resource leaves if and only
if its gate's recorded answer is no. Resolving those answers is where the
evidence lives, and each path has evidence that is complete on its own —
the deployer's input values on a direct deployment, the delivered
resource ids on a setup import, the recorded answers on an update. A gate
with no answer is not a decline; it is a gate nobody was asked, so the
resource stays and the frozen-compatibility check says what it always
said for ungated resources.

Deployments that predate recorded answers reconstruct them instead of
being special-cased. Presence in the settled state proves acceptance on
its own. Absence only means a decline for a gate the settled release
actually declared, so that release is read alongside the state — which
needs the deployment's current release to carry the settled stack, and in
push mode the manager was filling it with the target's. It now loads the
settled release, the way the pull path already did.
AWS emitted resource-scoped management grants by resource name and needed
gating; Azure merged them into one assignment and needed an OR gate. GCP
needs neither, and this records why rather than leaving it inferred: its
resource-scoped management path handles only Kubernetes clusters, a type
the gateability policy refuses, so a gated workload contributes no
management grant to begin with.

The test asserts both halves — nothing names the gated worker today, and
anything that ever does must carry its gate.
…lls on

Six functions each re-derived whether a gated resource is setup-created,
by hand, with the sense inverted in half of them. That classification now
lives in one pair of iterators, so a change to what counts as frozen has
one place to go instead of six.

The live gate's resolver returns where its answer came from, replacing a
second function that restated the same precedence rule and carried a
comment promising to mirror it — the audit log can no longer describe a
precedence the resolver did not apply.

The frozen strips get one ordered entry point. Their order is load-bearing
(the dominance pass needs the gating inputs computed before the frozen
strip removes the entries that define them) and was previously kept by
prose at three call sites.

GCP's binding gate asks the fragment whether a block is shared instead of
re-testing the provider type name the fragment already classified.

The manager holds the settled release's stack per deployment rather than
refetching and re-deserializing it on every tick of a running update.
…luded

A grant rendered on behalf of a resource the deployer can decline was
gated by the emitter that rendered it — three times, once per cloud —
because such a grant belongs to a resource other than the one whose
fragment it lives in, and the post-pass only ever sees a gated resource's
own fragment.

Emitters now declare those blocks the way they already declare shared
ones, and the generator installs the count. The clouds keep the shapes
they need: AWS one gate per policy, GCP a range of just-appended
bindings, Azure several gates on the one assignment it merges its
contributors into. That difference is now data — how many inputs a block
declares — rather than three implementations.

The reason to do it is the address bookkeeping. Blocks gated inside an
emitter never entered the generator's gated-address set, so neither the
reference rewrite nor the rendered-output scan knew about them; the GCP
helper documented this and named what made it safe — those blocks are
leaves nothing references — while the two added by this branch did not.
The invariant also could not hold for extension emitters outside this
crate, which is exactly who that helper is public for. Declared
contributions are registered, so the nets cover them.

Applied in the emit loop rather than a later pass: the GCP dedup folds
sibling counts together and needs them already installed.

Rendered output is unchanged — every snapshot is byte-identical.
The strip decision is cloud-agnostic and the gated render is linted by
terraform validate on all three providers, so only the applied declined
state would be new coverage. Same reasoning the file already applies to
the command-routing app.
@alongubkin

Copy link
Copy Markdown
Member

@greptile-apps

Comment thread crates/alien-manager/src/routes/stack.rs
…prove

Reconstructing a legacy deployment's frozen gate answers reads acceptance from
the settled state, which needs no release, but a decline is only legible
against the release that offered the gate. A missing release and one carrying
no stack for the platform were both collapsed into "no settled stack", so a
resource the deployer had declined could be accepted again in silence.

The contradiction the settled state can prove is still checked first, so a
flipped acceptance keeps naming its input instead of blaming the release.
…rator-owned-gating

# Conflicts:
#	crates/alien-terraform/src/emitters/gcp/kv.rs
…ien-330-gate-all-resources

# Conflicts:
#	crates/alien-deployment/src/pending.rs
#	crates/alien-deployment/src/updating.rs
Base automatically changed from itamar/alien-330-generator-owned-gating to main July 26, 2026 14:50
…-all-resources

# Conflicts:
#	crates/alien-cloudformation/tests/generator/gating_matrix_tests.rs
#	crates/alien-core/src/gateability.rs
#	crates/alien-terraform/src/emitter.rs
#	crates/alien-terraform/src/emitters/enabled.rs
#	crates/alien-terraform/src/gating.rs
#	crates/alien-terraform/src/generator.rs
#	crates/alien-terraform/tests/generator/gating_matrix_tests.rs
#	crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__enabled_gated_vault_aws.snap
#	crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__enabled_gated_vault_azure.snap
#	crates/alien-terraform/tests/generator/snapshots/generator__generator__helpers__enabled_gated_vault_gcp.snap
#	crates/alien-terraform/tests/generator/stack_input_tests.rs
#	packages/core/src/__tests__/gateability.test.ts
#	packages/core/src/generated/gateability.json
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

@alongubkin
alongubkin merged commit c6b8997 into main Jul 26, 2026
21 checks passed
@alongubkin
alongubkin deleted the itamar/alien-330-gate-all-resources branch July 26, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants