Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ See the CHANGELOG rule in [AGENTS.md](../AGENTS.md).

- QA Telegram probes, button presses and the `/start` preflight return once the bot's response stays
unchanged for 3 s; the wait stays the upper bound and silence still runs to it.
- A QA temporary-access grant or revoke calls the target's users-core capability with the stored
`USERS_GRANT_CAPABILITY` instead of redeploying the product, so QA access no longer costs a deploy.
- A temporary-access operation takes its Run's dispatch claim before any external write, and every revoke
re-derives the earlier-workflow stop from all the grant's Runs, so withdrawal or retry cannot skip either.

## 2026-10-17

Expand Down
3 changes: 2 additions & 1 deletion docs/contracts/generated-service-grants.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ intents, even when it carries a deployment SHA.
Temporary QA access uses the same generated-service capability but a distinct
durable record. It binds the central QA Telegram identity, application, base URL,
and SHA before dispatch. Grant and revoke each require the matching active or
inactive `/users/access` readback; the capability remains in deploy-local memory.
inactive `/users/access` readback without a redeploy; the stored capability is
decrypted only in the deploy consumer's memory.
The reconciler retries only the record's immutable target. Missing or stale grant
and revoke operation Runs consume their separately recorded, bounded attempt
budgets. A revoke that exceeds its attempt or unrevoked-time bound receives one
Expand Down
62 changes: 46 additions & 16 deletions docs/contracts/lifecycle-invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,26 +201,56 @@ older active deploys instead of allowing an earlier run to restore the value.
Canonical contracts: `dto/temporary_access.py` and `dto/qa_ssh_grant.py`.

Persist the immutable QA identity and exact deployed-service target before the
capability operation is dispatched. The post-health deploy worker resolves the
generated capability only in `secret_values`, then proves grant or revoke with
the matching access readback. A grant or revoke only ever acts on an existing
deployment of the grant's recorded `target_application_id`: the deploy consumer
reads that application's allocations and never creates any
(`existing_application_allocations`), and never asks the repository for "its"
application, since a repository can hold one per server. When the target exists
and holds no allocations it is not deployed: a revoke completes `SUCCESS` with
no precheck, SSH or DevOps run, because the access went with the deployment, and
the reconciler closes the grant as for any proved revoke; a grant fails
`owner_access_proof_failed` through the ordinary grant retry. A grant, target or
allocation set that cannot be read fails either operation closed with
`owner_access_proof_failed`, never as that proof. Both target columns are NOT NULL: every record has
capability operation is dispatched. The operation is a DEPLOY Run whose metadata
names the grant and `grant` or `revoke`; the deploy consumer validates it after
the deploy-lock claim and handles it before any product deploy preparation:
malformed or conflicting access metadata (a grant intent, a lifecycle action)
fails `owner_access_proof_failed` and never falls through to a product deploy.
It deploys nothing: no checkout, environment contract, secret generation or
persistence, GitHub secret or workflow dispatch, SSH, smoke, settings seeding or
`service_deployments` row. It reads only the grant's recorded
`target_application_id` and that application's existing allocations
(`existing_application_allocations`), never the repository's "its" application,
since a repository can hold one per server. When the target exists and holds no
allocations it is not deployed: a revoke completes `SUCCESS`, because the access
went with the deployment, and a grant fails `owner_access_proof_failed` through
the ordinary grant retry. A grant, target or allocation set that cannot be read
fails either operation closed with `owner_access_proof_failed`, never as that
proof. Either marker key in the Run metadata, even with a null value, makes the
Run such an operation. Before its first external write the operation takes its
Run's dispatch claim (`claim_dispatch`), the same locked admission a deploy takes
before GitHub, and renews it before each later external write; a withdrawal or a
lapsed lease that wins any renewal ends the Run `cancelled` with no further
external write, so a withdrawn operation never changes access. A revoke owes a
stop of earlier grant workflows when its message carries `fence_active_deploys`,
when any grant operation Run of the record crossed the dispatch boundary without
completing `SUCCESS`, or when the record's current grant Run is missing from that
history (`shared.temporary_access_cleanup.grant_cleanup_owes_workflow_stop`). The
history is the project's complete deploy Run listing filtered by the Run
metadata's grant id and operation, so replacing the record's grant Run does not
hide a predecessor; an unreadable listing fails the revoke. The scheduler derives
the flag this way on every revoke publication and the consumer re-derives it, so
no retry, replacement or redispatch loses it. The stop proves every
unfinished `deploy.yml` run of the repository stopped (`fence_active_deploys`),
renewing admission and checking the deploy lock immediately before each
cancellation request; it fails `owner_access_proof_failed` when a run cannot be
proved stopped, and a lost lock ends the Run `deploy_lock_lost`. Only then may a
revoke of an undeployed target record `SUCCESS`. For a deployed target the
consumer decrypts the project's stored `USERS_GRANT_CAPABILITY` in memory,
renews admission, re-reads the record and requires its project, target, base
URL, identity, head SHA, in-flight status and own Run id, checks the lease and
then the deploy lock with nothing awaited before the product call, and calls only
the target base URL's `/users/grant` or `/users/revoke` and `/users/access`
readback. A grant needs the identity active, a revoke inactive with no proof
failure; an absent or undecryptable capability, a refusal or an unverified
readback fails `owner_access_proof_failed` with a bounded reason and never mints
a capability. The terminal Run outcome is written last.
Both target columns are NOT NULL: every record has
a target. The target lock and partial unique index scope contention to
`(project_id, target_application_id)`.
Cancelled deploy-lock or fence operations are redispatched against their stored
target without consuming a grant or revoke proof budget; failed, missing, and
stale operations remain independently bounded. Immediately before either remote
operation, the executor re-reads the record and requires its own Run id and
matching in-flight state. Recovery changes that durable operation authority
stale operations remain independently bounded. Recovery changes that durable operation authority
before withdrawing the predecessor and dispatching fenced cleanup, so a delayed
grant cannot restore access after revoke proof. Cancelled revoke redispatches
retain their attempt budget only before the absolute unrevoked deadline.
Expand Down
Loading