Skip to content

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

Closed
ItamarZand88 wants to merge 10 commits into
feat/generator-owned-gatingfrom
feat/gate-all-resources
Closed

feat: gate every user resource — compute, email, and search#200
ItamarZand88 wants to merge 10 commits into
feat/generator-owned-gatingfrom
feat/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 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.
The internal protocol rules are explicit: an additive optional field
does not bump the version, and the earlier justification stretched the
actor-responsibility clause. 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.
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

The PR expands .enabled() gating across compute, email, and OpenSearch resources.

  • Splits frozen and live decline handling around deployment-time mutations.
  • Persists frozen-gate answers and rejects later conflicting answers.
  • Adds live-gate transition auditing and setup-import enforcement.
  • Extends TypeScript builders, generator matrices, state-machine tests, and an AWS Terraform end-to-end fixture.

Confidence Score: 3/5

The PR should not merge until updates correctly resolve and persist newly introduced frozen gates instead of silently omitting accepted resources.

A deployment that already records one frozen-gate answer skips resolution for later gates, after which state-based stripping removes a newly accepted frozen resource before compatibility checks and leaves its answer unrecorded.

Files Needing Attention: crates/alien-deployment/src/updating.rs and crates/alien-deployment/src/pending.rs

Important Files Changed

Filename Overview
crates/alien-deployment/src/pending.rs Splits frozen/live stripping, resolves and enforces gate answers, and audits live transitions; state-presence inference is central to update behavior.
crates/alien-deployment/src/updating.rs Reorders update preparation around the split strips but fails to resolve newly added frozen gates when another persisted answer already exists.
crates/alien-core/src/deployment/state.rs Adds backward-compatible persisted frozen-gate answers to runtime metadata.
crates/alien-manager/src/routes/stack.rs Records imported frozen answers and rejects conflicting setup re-registrations synchronously.
crates/alien-core/src/gateability.rs Broadens gateability policy to live compute and frozen email/OpenSearch resources.
crates/alien-preflights/src/compile_time/resource_enabled_valid.rs Allows gated compute while retaining rejection of ungated dependents on gated resources.
packages/core/src/worker.ts Adds the TypeScript .enabled() builder surface for workers.
packages/core/src/container.ts Adds the TypeScript .enabled() builder surface for containers.
packages/core/src/daemon.ts Adds the TypeScript .enabled() builder surface for daemons.
packages/core/src/email.ts Adds the TypeScript .enabled() builder surface for email resources.
packages/core/src/experimental/aws-opensearch.ts Adds the TypeScript .enabled() builder surface for experimental OpenSearch collections.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Deployment update] --> B[Load persisted frozen answers]
  B --> C{Answer map empty?}
  C -- Yes --> D[Resolve answers from current state]
  C -- No --> E[Keep existing map unchanged]
  D --> F[Enforce frozen-gate fixity]
  E --> F
  F --> G[Strip frozen resources absent from state]
  G --> H[Run mutations and compatibility checks]
  H --> I[Strip declined live resources]
  I --> J[Persist pending prepared stack and answers]
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/alien-deployment/src/updating.rs:81-87
**New frozen gates stay unresolved**

When an update adds a frozen gated resource to a deployment that already has another persisted gate answer, this nonempty-map guard skips resolution for the new gate. Fixity enforcement then has no entry to check, and the state-based frozen strip treats the not-yet-created resource as declined regardless of a supplied `true` value, causing the update to succeed without creating the resource or recording its answer.

Reviews (1): Last reviewed commit: "test(e2e): gate a worker pair in the ena..." | Re-trigger Greptile

Comment on lines +81 to +87
if persisted_gate_answers.is_empty() {
persisted_gate_answers = crate::pending::resolve_frozen_gate_answers(
&target_stack,
&stack_state,
&Default::default(),
)?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 New frozen gates stay unresolved

When an update adds a frozen gated resource to a deployment that already has another persisted gate answer, this nonempty-map guard skips resolution for the new gate. Fixity enforcement then has no entry to check, and the state-based frozen strip treats the not-yet-created resource as declined regardless of a supplied true value, causing the update to succeed without creating the resource or recording its answer.

Knowledge Base Used: Deployment Engine (alien-deployment)

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/alien-deployment/src/updating.rs
Line: 81-87

Comment:
**New frozen gates stay unresolved**

When an update adds a frozen gated resource to a deployment that already has another persisted gate answer, this nonempty-map guard skips resolution for the new gate. Fixity enforcement then has no entry to check, and the state-based frozen strip treats the not-yet-created resource as declined regardless of a supplied `true` value, causing the update to succeed without creating the resource or recording its answer.

**Knowledge Base Used:** [Deployment Engine (alien-deployment)](https://app.greptile.com/alien/-/custom-context/knowledge-base/alienplatform/alien/-/docs/deployment-engine.md)

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@ItamarZand88

Copy link
Copy Markdown
Contributor Author

Superseded by the same commits on a branch named per the PR validator; reopened as a new PR.

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.

1 participant