@astrale-domains/services 0.4.6 → 0.4.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/README.md +595 -43
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +1 -1
- package/dist/schema/modules/deployment/classes/cloudflare-deployment.d.ts +366 -0
- package/dist/schema/modules/deployment/classes/cloudflare-deployment.js +20 -0
- package/dist/schema/modules/deployment/classes/deployment.d.ts +93 -2
- package/dist/schema/modules/deployment/classes/deployment.js +27 -3
- package/dist/schema/modules/deployment/errors/deployment-hosting-unavailable.d.ts +7 -0
- package/dist/schema/modules/deployment/errors/deployment-hosting-unavailable.js +8 -0
- package/dist/schema/modules/deployment/errors/deployment-label-not-found.d.ts +12 -0
- package/dist/schema/modules/deployment/errors/deployment-label-not-found.js +11 -0
- package/dist/schema/modules/deployment/errors/deployment-not-found.d.ts +10 -0
- package/dist/schema/modules/deployment/errors/deployment-not-found.js +11 -0
- package/dist/schema/modules/deployment/errors/deployment-registry-only.d.ts +11 -0
- package/dist/schema/modules/deployment/errors/deployment-registry-only.js +12 -0
- package/dist/schema/modules/deployment/errors/index.d.ts +4 -0
- package/dist/schema/modules/deployment/errors/index.js +4 -0
- package/dist/schema/modules/deployment/functions/deploy-cloudflare-deployment.d.ts +170 -0
- package/dist/schema/modules/deployment/functions/deploy-cloudflare-deployment.js +14 -0
- package/dist/schema/modules/deployment/functions/describe-deployment-hosting.d.ts +8 -0
- package/dist/schema/modules/deployment/functions/describe-deployment-hosting.js +11 -0
- package/dist/schema/modules/deployment/functions/expire-deployment-previews.d.ts +16 -0
- package/dist/schema/modules/deployment/functions/expire-deployment-previews.js +14 -0
- package/dist/schema/modules/deployment/functions/index.d.ts +11 -0
- package/dist/schema/modules/deployment/functions/index.js +11 -0
- package/dist/schema/modules/deployment/functions/list-cloudflare-deployment-secret-names.d.ts +5 -0
- package/dist/schema/modules/deployment/functions/list-cloudflare-deployment-secret-names.js +4 -0
- package/dist/schema/modules/deployment/functions/list-cloudflare-deployments.d.ts +58 -0
- package/dist/schema/modules/deployment/functions/list-cloudflare-deployments.js +10 -0
- package/dist/schema/modules/deployment/functions/list-deployment-secret-names.d.ts +4 -0
- package/dist/schema/modules/deployment/functions/list-deployment-secret-names.js +6 -0
- package/dist/schema/modules/deployment/functions/read-deployment-last-calls.d.ts +10 -0
- package/dist/schema/modules/deployment/functions/read-deployment-last-calls.js +13 -0
- package/dist/schema/modules/deployment/functions/retain-deployment.d.ts +12 -0
- package/dist/schema/modules/deployment/functions/retain-deployment.js +13 -0
- package/dist/schema/modules/deployment/functions/retire-deployment.d.ts +23 -0
- package/dist/schema/modules/deployment/functions/retire-deployment.js +17 -0
- package/dist/schema/modules/deployment/functions/set-cloudflare-deployment-secret.d.ts +18 -0
- package/dist/schema/modules/deployment/functions/set-cloudflare-deployment-secret.js +7 -0
- package/dist/schema/modules/deployment/functions/set-deployment-secret.d.ts +14 -0
- package/dist/schema/modules/deployment/functions/set-deployment-secret.js +3 -0
- package/dist/schema/modules/deployment/index.d.ts +5 -3
- package/dist/schema/modules/deployment/index.js +4 -2
- package/dist/schema/modules/deployment/policies/act-for-organization.d.ts +6 -0
- package/dist/schema/modules/deployment/policies/act-for-organization.js +10 -0
- package/dist/schema/modules/deployment/policies/index.d.ts +1 -0
- package/dist/schema/modules/deployment/policies/index.js +1 -0
- package/dist/schema/modules/deployment/states/deployment-lifecycle.d.ts +6 -3
- package/dist/schema/modules/deployment/states/deployment-lifecycle.js +6 -4
- package/dist/schema/modules/deployment/types/deployment-hosting.d.ts +94 -0
- package/dist/schema/modules/deployment/types/deployment-hosting.js +19 -0
- package/dist/schema/modules/deployment/types/deployment-request.d.ts +161 -0
- package/dist/schema/modules/deployment/types/deployment-request.js +40 -0
- package/dist/schema/modules/deployment/types/deployment-response.d.ts +109 -0
- package/dist/schema/modules/deployment/types/deployment-response.js +36 -0
- package/dist/schema/modules/deployment/types/deployment-retention.d.ts +37 -1
- package/dist/schema/modules/deployment/types/deployment-retention.js +41 -1
- package/dist/schema/modules/deployment/types/deployment-retire.d.ts +30 -0
- package/dist/schema/modules/deployment/types/deployment-retire.js +18 -0
- package/dist/schema/modules/deployment/types/deployment-secret-digest.d.ts +17 -0
- package/dist/schema/modules/deployment/types/deployment-secret-digest.js +14 -0
- package/dist/schema/modules/deployment/types/deployment-secret.d.ts +59 -0
- package/dist/schema/modules/deployment/types/deployment-secret.js +52 -0
- package/dist/schema/modules/deployment/types/deployment-summary.d.ts +165 -0
- package/dist/schema/modules/deployment/types/deployment-summary.js +38 -0
- package/dist/schema/modules/deployment/types/frozen-configuration.d.ts +52 -0
- package/dist/schema/modules/deployment/types/frozen-configuration.js +55 -0
- package/dist/schema/modules/deployment/types/index.d.ts +18 -2
- package/dist/schema/modules/deployment/types/index.js +9 -1
- package/dist/schema/modules/service/classes/cloudflare-worker.d.ts +4 -4
- package/dist/schema/modules/service/classes/service.d.ts +2 -2
- package/dist/schema/modules/service/functions/set-worker-secret.d.ts +2 -2
- package/dist/schema/modules/service/types/legacy/service-deployment-content.js +8 -1
- package/dist/schema/schema.d.ts +456 -12
- package/dist/schema/schema.js +2 -1
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ protocols behind neutral SDK Integrations and stores only admitted lifecycle evi
|
|
|
30
30
|
same Mutation: the Kernel deletes a Node only together with every incident Edge. Its lines stay
|
|
31
31
|
registered with no holder and are refused to every organisation. A transfer is a root
|
|
32
32
|
`createEdge` of `line_owned_by` from the line to the new organisation; a line has one holder at
|
|
33
|
-
most.
|
|
33
|
+
most ([Transfer a line](#transfer-a-line)).
|
|
34
34
|
- secret values stay write-only at Cloudflare and never enter Astrale mutations or graph state.
|
|
35
35
|
- schedules remain provider state; only the owner may read or replace them.
|
|
36
36
|
- logs are remote evidence admitted by the Cloudflare Provider before a Workflow returns them.
|
|
@@ -46,6 +46,155 @@ protocols behind neutral SDK Integrations and stores only admitted lifecycle evi
|
|
|
46
46
|
never by its author, so Services refuses it as a caller variable or secret on every deployment
|
|
47
47
|
path and in secret management.
|
|
48
48
|
|
|
49
|
+
## Immutable deployments
|
|
50
|
+
|
|
51
|
+
The Admin-hosted Services runtime hosts publishers' releases as immutable deployments, one per
|
|
52
|
+
label, on the publisher's line. Three static `CloudflareDeployment` Methods serve them, and two
|
|
53
|
+
instance Methods rotate one deployment's secrets; any authenticated identity of the Admin instance
|
|
54
|
+
may call them. A Domain Worker serves every Kernel that
|
|
55
|
+
installs it, but deployments are hosted for the Admin instance's Kernel alone
|
|
56
|
+
(`SERVICES_DEPLOYMENT_KERNEL`): its graph is the line registry, while the deployments namespace and
|
|
57
|
+
the routing KV are shared. Any other Kernel finds hosting unavailable before anything is read.
|
|
58
|
+
|
|
59
|
+
- `deploy` takes a `DeploymentRequestV1`: the release's origin, environment, addressing, label,
|
|
60
|
+
build digest, frozen configuration, record fields, Worker bundle and the environment's current
|
|
61
|
+
secrets. It is a non-durable Action, never a Workflow, so no Execution, step result or journal
|
|
62
|
+
entry holds its input. Before any read it recomputes the build digest and the label, admits the
|
|
63
|
+
record with the SDK, and refuses a service binding, a router other than the logical
|
|
64
|
+
`admin-router`, a reserved binding name, or secrets other than exactly the declared ones. Only
|
|
65
|
+
the platform organisation (`SERVICES_PLATFORM_OWNER`) deploys on readable lines.
|
|
66
|
+
- `deploy` then observes the line under the Services Domain credential and proves, on the caller's
|
|
67
|
+
own session, that the caller acts for the organisation it names (`OwnLine` on a held line,
|
|
68
|
+
`ActForOrganization` for a new one) before any Mutation. A new label is created under a graph
|
|
69
|
+
lease. The provider uploads it create-only, with `ASTRALE_BUILD_DIGEST`, `ASTRALE_SETTINGS` (router
|
|
70
|
+
binding `ROUTER`), a signing key Services generates for this deployment alone, and the request's
|
|
71
|
+
secrets. It adopts a script only when resuming a deployment the graph already held from an earlier
|
|
72
|
+
attempt, and only when the script's tags name the same artifact, line and key and this Kernel's
|
|
73
|
+
installation; any other script under the label is another deployment's, refused
|
|
74
|
+
`deployment-conflict`, and its secrets are never written. It checks that the uploaded version
|
|
75
|
+
itself serves before it activates the label through the routing KV `record:<label>`. The graph then records the deployment active with its key id, script etag and
|
|
76
|
+
a keyed HMAC-SHA256 digest of its secrets; no value, private key or version id is stored.
|
|
77
|
+
- the same release again is the same label: `deploy` answers `reused`. Its secrets are the
|
|
78
|
+
environment's current values: when they match the recorded digests it answers `unchanged` and
|
|
79
|
+
writes nothing; otherwise it rewrites the changed values in place on that deployment alone and
|
|
80
|
+
answers `updated` (same script, URL, issuer and key, no new release). Every other deployment of
|
|
81
|
+
the line keeps its own secrets.
|
|
82
|
+
- `setSecret({ name, value })` and `secrets({})` are instance Methods of one `CloudflareDeployment`.
|
|
83
|
+
`setSecret` rewrites one secret in place on that deployment only, and only a name its frozen
|
|
84
|
+
configuration declares (`secret-undeclared` otherwise); there is no `deleteSecret`, since a bound
|
|
85
|
+
name is part of the release, and the signing key never rotates. `secrets` lists the names its
|
|
86
|
+
script binds, never a value. Like `deploy`, any authenticated identity may call them, because a
|
|
87
|
+
publisher holds no `can_use` on Services' Functions and an `authorized` Method requires it of
|
|
88
|
+
every non-Root caller: before anything is read, the Kernel answers `OwnDeployment` for the caller
|
|
89
|
+
on its own session, and any other caller finds no such deployment (`DEPLOYMENT_NOT_FOUND`). If the
|
|
90
|
+
Admin instance ever grants publishers `can_use` on these Methods, they become `authorized` with
|
|
91
|
+
the `OwnDeployment` Policy instead. Both are non-durable Actions, so no Execution, step result or
|
|
92
|
+
journal entry holds a value.
|
|
93
|
+
- a `setSecret` refusal is `{ kind: 'rejected', reason, message, retryAfter? }`.
|
|
94
|
+
`service-unavailable` with `retryAfter` (the deployment is being created, another operation
|
|
95
|
+
holds it, or the provider failed) is retried then; without `retryAfter` the deployment cannot be
|
|
96
|
+
rotated in place (its creation failed, it is retiring or retired, or the script under its label is
|
|
97
|
+
not the recorded one, which is the operator's to inspect), and the message says what can.
|
|
98
|
+
- an in-place rewrite, by `deploy` or `setSecret`, runs under the deployment's graph lease. It
|
|
99
|
+
compares the values with the keyed digests and writes only the changed ones; it first forgets the
|
|
100
|
+
digests of the names it writes and records the new digests only once every write succeeded, so a
|
|
101
|
+
recorded digest always names the value last written. A provider failure answers
|
|
102
|
+
`service-unavailable` with `retryAfter`, and a rerun, with any values, converges. The script is
|
|
103
|
+
first checked to be the recorded one (code hash, artifact tag, key tag, installation tag); a
|
|
104
|
+
rewrite never touches another script. Workers for Platforms keeps the script's name, code hash,
|
|
105
|
+
tags and bindings across a secret write and mints a new provider version, which serves the new
|
|
106
|
+
value a few seconds later (OPS-2 X1): propagation is eventually consistent, and nothing records a
|
|
107
|
+
version id.
|
|
108
|
+
- `hosting` returns the routing domain and how the caller's lines read (readable for the platform
|
|
109
|
+
organisation, opaque otherwise). `list` returns the deployments of the lines the caller holds. It
|
|
110
|
+
reads the held lines under the Services Domain credential, narrowed in the Query to the origin
|
|
111
|
+
and environment asked for, and keeps the lines a batched `Identity.can(OwnLine)` grants the
|
|
112
|
+
caller: a caller-mode read sees none of the lines a caller holds through `extends_with`
|
|
113
|
+
([kernel#1138](https://github.com/astrale-os/kernel/issues/1138), the removal trigger of this
|
|
114
|
+
filter). One listing reads at most 16,384 matching held lines and fails beyond, so a listing
|
|
115
|
+
without an origin is bounded by the platform's held lines. A deployment whose stored record or
|
|
116
|
+
configuration this Services SDK no longer admits, for instance after a stricter decoder, is
|
|
117
|
+
still listed, from the facts Services owns, with its record exactly as stored (typed as JSON for
|
|
118
|
+
that row): the listing never fails on it, and the consumer admits the record itself. Deploy,
|
|
119
|
+
retain, retire, `setSecret`, `secrets` and `expirePreviews` fail closed on such a deployment
|
|
120
|
+
with an undeclared internal error (5000) and write nothing; `lastCalls` answers from its facts.
|
|
121
|
+
- deleting a Service, or an instance and its Services, never addresses a publisher's deployment.
|
|
122
|
+
|
|
123
|
+
Only a runtime whose Environment vars set `SERVICES_DEPLOYMENT_HOST` (the deployments' routing
|
|
124
|
+
domain) hosts deployments; every other runtime answers `hosting-unavailable`. Such a runtime refuses
|
|
125
|
+
to boot without `SERVICES_DEPLOYMENT_KERNEL` (the Admin Kernel's issuer URL),
|
|
126
|
+
`SERVICES_SECRET_DIGEST_KEY` (at least 32 UTF-8 bytes, kept for the life of the data plane: the
|
|
127
|
+
recorded digests depend on it), the `SERVICE_DEPLOYMENTS` dispatch binding to the environment's
|
|
128
|
+
deployments namespace, and `CLOUDFLARE_SCHEDULE_KV_ID`, the routing KV that activates a label, and
|
|
129
|
+
refuses a `SERVICES_PLATFORM_OWNER` that is not an Identity id. Hosting is declared once, by the
|
|
130
|
+
Environment's `vars` in `astrale.config.ts`: the same vars decide the `SERVICE_DEPLOYMENTS` binding,
|
|
131
|
+
and the configuration refuses a secrets file that sets a hosting var. [Real-Kernel
|
|
132
|
+
proof](tests/kernel/README.md).
|
|
133
|
+
|
|
134
|
+
### Retention and retirement
|
|
135
|
+
|
|
136
|
+
A deployment is a `preview` until the registry Domain marks it `published`; only a preview expires.
|
|
137
|
+
Four more `CloudflareDeployment` Methods manage what stays served. Retirement, retention and
|
|
138
|
+
revival name a deployment by its label, script etag, artifact tag and configuration digest, never
|
|
139
|
+
by a provider version id.
|
|
140
|
+
|
|
141
|
+
- `retire` is the instance Method of the line holder's emergency stop. Any authenticated identity
|
|
142
|
+
may call it, because a publisher holds no `can_use` on Services' Functions and an `authorized`
|
|
143
|
+
Method requires it of every non-Root caller: before anything is read, the Kernel answers
|
|
144
|
+
`OwnDeployment` for the caller on its own session, and any other caller finds no such deployment
|
|
145
|
+
(`DEPLOYMENT_NOT_FOUND`). If the Admin instance ever grants publishers `can_use` on it, it becomes
|
|
146
|
+
`authorized` with the `OwnDeployment` Policy instead. It withdraws the deployment for good in two
|
|
147
|
+
phases. The graph records it `retiring` first, so no deploy reuses it; the same Mutation evicts
|
|
148
|
+
the operation lease of any operation writing the script, such as an in-place rewrite of its
|
|
149
|
+
secrets, which then records nothing. Then the routing KV gets the tombstone
|
|
150
|
+
`retirement:<label>` = `withdrawn`. The dispatcher
|
|
151
|
+
answers a label with a tombstone `DEPLOYMENT_RETIRED` (`410`) and keeps serving its
|
|
152
|
+
`deployment.json` from `record:<label>`, which stays. The first call answers `retiring` with
|
|
153
|
+
`retryAfterMs`. A call 65 seconds after the tombstone, once the dispatcher's retirement probe
|
|
154
|
+
answers `DEPLOYMENT_RETIRED`, deletes the script and its signing key and answers `retired`. A
|
|
155
|
+
withdrawn label is never served again: deploying the same release is refused
|
|
156
|
+
`deployment-retired`.
|
|
157
|
+
- `retain` marks one preview `published`. Only the registry Domain's principal calls it, acting as
|
|
158
|
+
itself; it is the installed, unfrozen Domain whose origin is `SERVICES_REGISTRY_ORIGIN`. Services
|
|
159
|
+
reads the caller's Domain node by id under its own credential, which is why it requires read on
|
|
160
|
+
the Kernel's `Domain` Class; nothing the caller presents decides it. An authenticated caller
|
|
161
|
+
carries Identity ids but no Domain origin, and read on `Identity` does not reach a Domain's node.
|
|
162
|
+
Every instance that installs this Services version grants that read, so Services can read the
|
|
163
|
+
origin and frozen flag of each Domain installed there; only the Admin instance's hosting runtime
|
|
164
|
+
uses it. The requirement goes once the Kernel gives a handler its caller's Domain origin. A line
|
|
165
|
+
holder, or an identity acting for the registry, is refused `DEPLOYMENT_REGISTRY_ONLY`. The mark
|
|
166
|
+
is monotonic and idempotent, and it never undoes a retirement in progress: a preview marked after
|
|
167
|
+
its expiry started still retires, and a redeploy of the same release revives it.
|
|
168
|
+
- `lastCalls` returns the last call of up to 100 labels the caller holds (as the Kernel answers
|
|
169
|
+
`OwnDeployment` on the caller's session), or any labels for the registry; an unknown label and
|
|
170
|
+
another organisation's both answer `DEPLOYMENT_LABEL_NOT_FOUND`. It reads the data points the dispatcher writes per admitted call, through the Analytics
|
|
171
|
+
Engine SQL API, and the last call recorded in the graph. A label nobody called reads `null`. It
|
|
172
|
+
reads only the facts Services recorded, so it answers for a listed deployment whose stored
|
|
173
|
+
record this SDK no longer admits.
|
|
174
|
+
- `expirePreviews` is the registry's pass. A preview expires 30 days after its activation (its
|
|
175
|
+
creation, or its revival) or its last call, whichever is later; every activation records its
|
|
176
|
+
instant, and a pass refuses an active preview without one rather than expire it on its creation. The pass is a plan unless the runtime sets
|
|
177
|
+
`SERVICES_PREVIEW_EXPIRY=enforce` and the caller passes `apply: true`: it answers the `planned`
|
|
178
|
+
labels and retires none. Applied, it retires them in two phases with the tombstone `expired` and
|
|
179
|
+
answers `retired` and `pending` labels; the next pass completes the pending ones. Either way it
|
|
180
|
+
records later calls, so each preview's `expiresAt` follows its traffic. `limit` bounds the
|
|
181
|
+
deployment writes of one pass (64 by default), and retirements come first.
|
|
182
|
+
|
|
183
|
+
An expired label revives. When the same owner deploys the same release (same label, so same
|
|
184
|
+
artifact and configuration) again, the deployment resumes from `retired`. It is uploaded
|
|
185
|
+
create-only with a new signing key, and the activation republishes the same `record:<label>`
|
|
186
|
+
before it removes the `expired` tombstone. An instance that pins the label is served the same
|
|
187
|
+
release digest at the same URL. A `withdrawn` tombstone is never removed.
|
|
188
|
+
|
|
189
|
+
`SERVICES_REGISTRY_ORIGIN` and `SERVICES_PREVIEW_EXPIRY` (`plan`, the default, or `enforce`) are
|
|
190
|
+
Environment vars of a hosting runtime, refused in a secrets file and refused without
|
|
191
|
+
`SERVICES_DEPLOYMENT_HOST`. Without a registry origin no caller is the registry, and `retain` and
|
|
192
|
+
`expirePreviews` are refused. `admin` and the frozen `admin-beta` name `admin.astrale.ai` and keep
|
|
193
|
+
expiry a plan.
|
|
194
|
+
Reading last calls needs the API token's Account Analytics Read permission. `pnpm
|
|
195
|
+
provider:deployment-audit` lists the deployments namespace read-only
|
|
196
|
+
([operator](providers/cloudflare/operator/README.md)).
|
|
197
|
+
|
|
49
198
|
The View, CLI, and agent consumers read the named Service Queries directly; there is intentionally
|
|
50
199
|
no list Function wrapping `listServicesQuery`. Deployment, secrets, schedules, logs, and deletion
|
|
51
200
|
are Workflows because they coordinate admitted caller intent with external effects. Receiver Methods
|
|
@@ -58,6 +207,246 @@ applies `OwnService` to each one. Workflow addresses are `CloudflareWorker.*`; a
|
|
|
58
207
|
keys are not executable. A future provider Class must supply its own concrete declarations and
|
|
59
208
|
implementations. Properties and ownership relationships retain their inherited meaning.
|
|
60
209
|
|
|
210
|
+
## Operating hosted deployments
|
|
211
|
+
|
|
212
|
+
### Recorded choices
|
|
213
|
+
|
|
214
|
+
The deployment model left four choices open (decisions D12, D13 and D16 of the deployment and
|
|
215
|
+
versioning spec, and the rule for deployment secrets). Services implements them as follows.
|
|
216
|
+
|
|
217
|
+
| Choice | Implemented here |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| How a line reads in a deployment URL | Only the platform organisation (`SERVICES_PLATFORM_OWNER`) registers or deploys readable lines, which read `<readable>-<fingerprint>`, such as `agencies-staging-svkrguuhpcvmxxas`. Every other line is its 16-character fingerprint alone, derived from the origin and environment, never a stored UUID. A line keeps the addressing it was registered with, whoever holds it later: a deploy with the other addressing is refused `address-mismatch`, and a readable deploy for any other organisation `invalid-deployment`. |
|
|
220
|
+
| Which deployment runs a Domain's crons | None. A hosted deployment's frozen configuration has no trigger, a Workers for Platforms script cannot own one, and the dispatcher's minute fan-out reads only the `cron:` keys of Services, which no deployment has. The spec's recommendation, crons on the deployment of the version the author designates (by default the highest stable one), needs the registry and is not implemented by the foundation. |
|
|
221
|
+
| Where deployments live | In the Admin instance's Services, on a line held by the publisher's organisation, outside every instance that installs them. Uninstalling a Domain, deleting an instance or deleting its Services never touches them. The organisation is an Identity of the Admin instance that the deploy request names; a request that names none deploys for the caller itself. |
|
|
222
|
+
| Deployment secrets | A deploy binds the environment's current secrets to the new deployment only; every other deployment keeps its own. A secret rotates in place on one deployment, which keeps its script, URL, issuer and key: by `setSecret`, or by deploying the identical release again. The signing key never rotates. |
|
|
223
|
+
|
|
224
|
+
### Rotate a deployment's secrets
|
|
225
|
+
|
|
226
|
+
Rotating a secret never creates a deployment. Two calls write a new value in place on one
|
|
227
|
+
deployment, and on no other:
|
|
228
|
+
|
|
229
|
+
- deploying the identical release again (same build and configuration) with the new value in the
|
|
230
|
+
environment's secrets: it is the same label, `deploy` answers `reused`, and its `secrets.state` is
|
|
231
|
+
`updated` when a value changed;
|
|
232
|
+
- `setSecret` on the deployment, for one name its frozen configuration declares.
|
|
233
|
+
|
|
234
|
+
A deployment is called by the `id` that `list` returns. Every call below runs on the Admin instance
|
|
235
|
+
(`--admin`, or `--admin=<bookmark>`), as an identity that acts for the organisation holding the line
|
|
236
|
+
(`--as <identity>` when it is not the bookmark's default), with an `astrale` CLI whose `call` takes
|
|
237
|
+
`--admin` and `-d @file`. Put `key=value` params before `--admin`.
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
astrale call /:services.astrale.ai:class.CloudflareDeployment:list \
|
|
241
|
+
origin=agencies.example.com environment=staging --admin --json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`list` answers `{ "deployments": [...] }` with each deployment's `id`, `label`, `url`, `state` and
|
|
245
|
+
declared `record`.
|
|
246
|
+
|
|
247
|
+
Never pass a secret value in argv, where `ps` and shell history keep it. Write the input to a file
|
|
248
|
+
that only you can read (`umask 077`, then your editor), outside the repository:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{ "name": "API_TOKEN", "value": "<the new value>" }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.setSecret" \
|
|
256
|
+
--admin -d @rotate.json --json
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
or pipe it from your secret manager on stdin (`read-new-secret` stands for the command that prints
|
|
260
|
+
only the new value):
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
read-new-secret | jq -Rs '{ name: "API_TOKEN", value: rtrimstr("\n") }' \
|
|
264
|
+
| astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.setSecret" \
|
|
265
|
+
--admin -d - --json
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`secrets` lists the names a deployment binds, never a value:
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.secrets" --admin --json
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
A refusal is the call's value (`"kind": "rejected"`); the two errors fail the call.
|
|
275
|
+
|
|
276
|
+
| Reply | Meaning | Then |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `{ "name": "API_TOKEN" }` | The value is written, or was already the one served. Propagation is eventually consistent: the provider serves a new value about 4 s later. | Nothing. Calls in those seconds may still see the old value. |
|
|
279
|
+
| `secret-undeclared` | The deployment's frozen configuration binds no such name. Nothing was written. | A new name is a new release: declare it and deploy. |
|
|
280
|
+
| `service-unavailable` with `retryAfter` | The deployment is being created, another operation holds it, or the provider failed. | The same call again after `retryAfter`. A rerun converges, whatever an interrupted one wrote. |
|
|
281
|
+
| `service-unavailable` without `retryAfter` | The deployment cannot be rotated in place: its creation failed, it is retiring or retired, or the script under its label is not the recorded one. `message` says which. | Deploy the release again after a failed creation. A retired deployment serves nothing. Ask the Services operator about a script that is not the recorded one. |
|
|
282
|
+
| `DEPLOYMENT_NOT_FOUND` | The caller neither holds the deployment's line nor acts for its holder. Nothing was read. | Call as an identity of the organisation holding the line. |
|
|
283
|
+
| `DEPLOYMENT_HOSTING_UNAVAILABLE` | The call reached a Kernel other than the Admin instance's, or the Services runtime there hosts no deployments (its Environment sets no `SERVICES_DEPLOYMENT_HOST`). | Call with `--admin`. If the Admin instance still answers it, ask the Services operator. |
|
|
284
|
+
|
|
285
|
+
### A leaked secret
|
|
286
|
+
|
|
287
|
+
A deploy never rewrites another deployment's secrets, so a leaked value stays bound to every
|
|
288
|
+
deployment that still serves it until each one is rotated:
|
|
289
|
+
|
|
290
|
+
1. Put the new value everywhere a deploy reads the environment's secrets from: the secrets file the
|
|
291
|
+
environment's adapter names (its `secrets` option, such as `.env.staging`) and every CI secret
|
|
292
|
+
store that deploys the environment. From then on, until step 5, no deploy may run with the old
|
|
293
|
+
value: an identical redeploy with it would write it back in place (`secrets.state: updated`).
|
|
294
|
+
2. Rotate every deployment of that origin and environment that serves, with the input file above.
|
|
295
|
+
`list` shows a deployment only once it has served: a deployment still being created, or whose
|
|
296
|
+
creation failed, is not listed. The block reads the list on each run, so run it again until
|
|
297
|
+
every reply is `{ "name": ... }`. A deployment already rotated answers the same without writing
|
|
298
|
+
anything, and one that answered `service-unavailable` with `retryAfter` takes the rotation on a
|
|
299
|
+
later run.
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
for ID in $(astrale call /:services.astrale.ai:class.CloudflareDeployment:list \
|
|
303
|
+
origin=agencies.example.com environment=staging --admin --json \
|
|
304
|
+
| jq -r '.deployments[] | select(.state == "active") | .id'); do
|
|
305
|
+
astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.setSecret" \
|
|
306
|
+
--admin -d @rotate.json --json
|
|
307
|
+
done
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
An identical redeploy rotates only the deployment of the current release; older ones need
|
|
311
|
+
`setSecret`. Only `active` deployments rotate in place: a retiring or retired one answers
|
|
312
|
+
`service-unavailable` without `retryAfter`, so revoking the value at its source (step 5) is what
|
|
313
|
+
covers it. A deployment still being created, or whose creation failed, is step 3's.
|
|
314
|
+
3. Take over every creation that a deploy started before step 1: that deploy may have read the old
|
|
315
|
+
value. Its deployment stays `deploying`, or `failed`, until a deploy records it active, so `list`
|
|
316
|
+
does not show it and `setSecret` refuses it; yet it serves as soon as its deploy has published
|
|
317
|
+
its record. Lease expiry alone does not stop that deploy. Activation checks only that the
|
|
318
|
+
deployment still holds the lease the deploy took, so a deploy that stalls past its lease, which
|
|
319
|
+
ends 10 minutes after it started, can still activate the deployment with the old value until
|
|
320
|
+
another deploy takes it over.
|
|
321
|
+
|
|
322
|
+
Find these deployments on the Admin instance (`ADMIN_INSTANCE` is its instance bookmark, as in
|
|
323
|
+
[Transfer a line](#transfer-a-line)). The query answers the deployments of the lines you hold or
|
|
324
|
+
act for, those still being created included. Keep the `label` of each one whose `state` is
|
|
325
|
+
`deploying` or `failed` and whose `record` names this origin and environment. When the answer
|
|
326
|
+
has a `page.next`, the same query with `--cursor <page.next>` reads the next page.
|
|
327
|
+
|
|
328
|
+
```sh
|
|
329
|
+
astrale query --class /:services.astrale.ai:class.CloudflareDeployment --limit 100 --json \
|
|
330
|
+
-i "$ADMIN_INSTANCE"
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
For each label, deploy its release again, identical (the same build and configuration; with
|
|
334
|
+
adapter-astrale, `astrale-domain deploy <environment>` from the same source), now that the
|
|
335
|
+
secrets file holds the new value. Repeat until it answers `deployed` for that label:
|
|
336
|
+
- while the stalled deploy's lease runs, it answers `service-unavailable` with `retryAfter`, the
|
|
337
|
+
end of that lease: deploy again then;
|
|
338
|
+
- once the lease has expired, it takes the deployment over, so the stalled deploy can no longer
|
|
339
|
+
activate it, and creates it with the current secrets, rewriting them on any script the stalled
|
|
340
|
+
deploy left;
|
|
341
|
+
- after a failed creation it retries with the current secrets; once the stalled deploy has
|
|
342
|
+
activated the deployment, it reuses it and rewrites its secrets in place
|
|
343
|
+
(`secrets.state: updated`).
|
|
344
|
+
|
|
345
|
+
A release that does not rebuild identically has another label and takes nothing over: the
|
|
346
|
+
stalled deployment keeps the value its deploy wrote until step 5 revokes it. Then run step 2's
|
|
347
|
+
block once more.
|
|
348
|
+
4. Deployments in the platform dispatch namespace have no Services node, and neither `list` nor the
|
|
349
|
+
query shows them. They are the platform Domains deployed through adapter-cloudflare's namespace
|
|
350
|
+
mode: Shell, the Services runtime, Issues, ai-gateway, closing, grc, integrations and ui, each
|
|
351
|
+
once it has moved there. Rotate each one in place with the platform namespace's tooling, with
|
|
352
|
+
its operator file sourced
|
|
353
|
+
([operator README](./providers/cloudflare/operator/README.md#operating-platform-deployments)): one
|
|
354
|
+
label and one declared name per run, the value read on stdin from a file that holds only the new
|
|
355
|
+
value, outside the repository and readable only by you.
|
|
356
|
+
|
|
357
|
+
```sh
|
|
358
|
+
pnpm provider:platform-secret -- --label <label> --name API_TOKEN < new-value
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Or deploy its identical release again. A platform deploy that started before step 1 may have
|
|
362
|
+
read the old value too: once it has returned, rotate the deployment it made or reused. Admin is
|
|
363
|
+
not one of them: it runs on its own bootstrap Worker, and its secrets rotate through that
|
|
364
|
+
Worker's own deploy tooling. The Services runtime is one of them once the Admin instance runs on
|
|
365
|
+
a deployment of [`admin`](#admin-hosted-runtime); its identical redeploy is
|
|
366
|
+
`pnpm run deploy admin`, with the new value in `.env.admin`. Until that move it is the frozen
|
|
367
|
+
`admin-beta` Worker: put the new value in `.env.admin-beta` (and the same in `.env.admin`, which
|
|
368
|
+
keeps its values) and deploy it again with `pnpm run deploy admin-beta`, from the commit it was
|
|
369
|
+
last deployed from so that only its secrets change.
|
|
370
|
+
5. Revoke the leaked value where it was issued once step 2's block answered `{ "name": ... }` for
|
|
371
|
+
every deployment and every redeploy of step 3 answered `deployed`, or at once when the leak
|
|
372
|
+
outweighs the failures of deployments not rotated yet.
|
|
373
|
+
6. Delete `rotate.json` and `new-value`.
|
|
374
|
+
|
|
375
|
+
### Transfer a line
|
|
376
|
+
|
|
377
|
+
A line belongs to the first organisation that deploys on it. No Services Method moves it: an
|
|
378
|
+
operator transfers it as the Admin instance's root, with one Mutation on its `line_owned_by` Edge.
|
|
379
|
+
Its deployments stay on the line and keep serving. From that Mutation on, the new holder lists,
|
|
380
|
+
rotates and deploys them, and the previous holder and its members no longer see them.
|
|
381
|
+
|
|
382
|
+
1. Find the line and its holder on the Admin instance (`ADMIN_INSTANCE` is its bookmark,
|
|
383
|
+
`ADMIN_ROOT` its root identity, `LINE` the line's id):
|
|
384
|
+
|
|
385
|
+
```sh
|
|
386
|
+
astrale query --class /:services.astrale.ai:class.DeploymentLine --limit 100 --json \
|
|
387
|
+
-i "$ADMIN_INSTANCE" --as "$ADMIN_ROOT"
|
|
388
|
+
astrale query "@$LINE" --edge /:services.astrale.ai:class.line_owned_by \
|
|
389
|
+
--direction outgoing --json -i "$ADMIN_INSTANCE" --as "$ADMIN_ROOT"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Pick the line whose `origin` and `environment` are the ones to transfer. The first query reads
|
|
393
|
+
one page: when its answer has a `page.next`, more lines exist, and the same query with
|
|
394
|
+
`--cursor <page.next>` reads the next page.
|
|
395
|
+
|
|
396
|
+
2. Write the Mutation. A held line moves in one atomic Mutation that deletes the current holder's
|
|
397
|
+
Edge and creates the new one; if the current holder is not the one named, nothing moves:
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"preconditions": [],
|
|
402
|
+
"operations": [
|
|
403
|
+
{
|
|
404
|
+
"op": "edge.delete",
|
|
405
|
+
"edge": {
|
|
406
|
+
"class": "services.astrale.ai:class.line_owned_by",
|
|
407
|
+
"source": "@<line-id>",
|
|
408
|
+
"target": "@<current-holder-id>"
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"op": "edge.create",
|
|
413
|
+
"class": "services.astrale.ai:class.line_owned_by",
|
|
414
|
+
"source": "@<line-id>",
|
|
415
|
+
"target": "@<new-organization-id>",
|
|
416
|
+
"props": {}
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
A line without a holder, after its organisation was deleted, takes the `edge.create` operation
|
|
423
|
+
alone. The Kernel refuses a second holder, so that form never moves a held line.
|
|
424
|
+
|
|
425
|
+
3. Check the document offline, then apply it:
|
|
426
|
+
|
|
427
|
+
```sh
|
|
428
|
+
astrale mutate --file transfer.json --dry
|
|
429
|
+
astrale mutate --file transfer.json -i "$ADMIN_INSTANCE" --as "$ADMIN_ROOT"
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
A readable line stays readable: give it only to the platform organisation, since any other one is
|
|
433
|
+
refused on it. Changing `SERVICES_PLATFORM_OWNER` therefore means transferring every readable line
|
|
434
|
+
to the new platform organisation. Deleting an organisation that holds lines leaves them without a
|
|
435
|
+
holder (see [Model](#model)): their deployments keep serving, and nobody can list, rotate or deploy
|
|
436
|
+
them until they are transferred.
|
|
437
|
+
|
|
438
|
+
### Orphans
|
|
439
|
+
|
|
440
|
+
A hosted deployment belongs to its line, never to the instances that install it: deleting or
|
|
441
|
+
retiring an instance leaves it alone, and the orphan audit and reclaim of the
|
|
442
|
+
[operator README](./providers/cloudflare/operator/README.md) never select it. What can be left over:
|
|
443
|
+
|
|
444
|
+
| Situation | What holds | Then |
|
|
445
|
+
| --- | --- | --- |
|
|
446
|
+
| A deploy interrupted before activation | The graph keeps the deployment `deploying` under its lease, or `failed`, and `list` does not show it. An uploaded script serves nothing until its deploy publishes its record; one interrupted after that serves while the graph still says `deploying` or `failed`. Until another deploy takes it over, the interrupted deploy can still activate it, even after its lease expired. | Deploy the same release again. It resumes on the same label, takes over an expired lease, and adopts the uploaded script only if its tags name the same artifact, line, key and Admin Kernel, rewriting its secrets with the environment's current ones. |
|
|
447
|
+
| A line whose organisation was deleted | No organisation holds it; every deploy on it is refused `line-owned-elsewhere`. Its deployments keep serving, and nobody can rotate them. | Transfer the line. |
|
|
448
|
+
| A script under a label that is not the recorded one | `deploy` answers `deployment-conflict` and `setSecret` `service-unavailable` without `retryAfter`; nothing is written to it. | The operator compares its `astrale-artifact:`, `astrale-line:`, `astrale-key:` and `astrale-install:` tags with the deployment's record and key. |
|
|
449
|
+
|
|
61
450
|
## Shared public ingress
|
|
62
451
|
|
|
63
452
|
New Services use the environment's proxied wildcard DNS, wildcard Worker route and wildcard
|
|
@@ -109,46 +498,73 @@ data.
|
|
|
109
498
|
|
|
110
499
|
## Admin-hosted runtime
|
|
111
500
|
|
|
112
|
-
The
|
|
113
|
-
publishers' deployments outside the instances that install them.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
(`ASTRALE_ROUTER_WORKER`), the routing domain under which deployments are addressed
|
|
117
|
-
(`SERVICES_DEPLOYMENT_HOST`, served by the Services dispatcher)
|
|
118
|
-
(`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
501
|
+
The Admin-hosted runtime is the Services runtime of the Admin instance, the one that hosts
|
|
502
|
+
publishers' deployments outside the instances that install them. Only the Admin instance installs
|
|
503
|
+
it; consumer instances keep the Services they pin. It runs on the beta data plane the Admin
|
|
504
|
+
instance's Services already manage. Its plain vars name the router Worker
|
|
505
|
+
(`ASTRALE_ROUTER_WORKER`), the routing domain under which hosted deployments are addressed
|
|
506
|
+
(`SERVICES_DEPLOYMENT_HOST`, served by the Services dispatcher), the Admin instance's Kernel, the
|
|
507
|
+
only one it hosts deployments for (`SERVICES_DEPLOYMENT_KERNEL`, `https://admin.eu.beta.astrale.ai/api`),
|
|
508
|
+
and the registry Domain (`SERVICES_REGISTRY_ORIGIN`). These vars are the only declaration of
|
|
509
|
+
hosting: they also bind `SERVICE_DEPLOYMENTS`. Its secrets file must set
|
|
510
|
+
`SERVICES_SECRET_DIGEST_KEY`, the key of each hosted deployment's secret digest, and the dispatch
|
|
511
|
+
namespace and routing KV the runtime boots with. The runtime keys HMAC-SHA256 with the UTF-8 bytes
|
|
512
|
+
of the digest key as written, never base64-decoded, so the configuration refuses, before any
|
|
513
|
+
deployment effect, a file without them, a digest key shorter than 32 UTF-8 bytes, or a file that
|
|
514
|
+
sets a hosting var. `SERVICES_PLATFORM_OWNER` is not set yet: until it names the platform
|
|
515
|
+
organisation's Identity id on the Admin instance, every line is opaque and a readable deploy is
|
|
516
|
+
refused.
|
|
517
|
+
|
|
518
|
+
Two Environments deploy it:
|
|
519
|
+
|
|
520
|
+
- **`admin`**, the canonical one. Every `pnpm run deploy admin` makes one immutable deployment of
|
|
521
|
+
the release in the platform dispatch namespace (`astrale-platform`), on the readable line
|
|
522
|
+
`services-admin-6ze5tudscrzgbrbv`, at `https://<label>.platform.astrale.ai`. The deployment is
|
|
523
|
+
its own issuer, signs with a key generated for it, and binds its candidate namespace
|
|
524
|
+
(`SERVICE_CANDIDATES`) and the deployments namespace (`SERVICE_DEPLOYMENTS`) at its create-only
|
|
525
|
+
upload, both derived from `CLOUDFLARE_DISPATCH_NAMESPACE`. Nothing is ever replaced in place: a
|
|
526
|
+
new release is a new URL, which the Admin instance moves to by a consented install. Redeploying
|
|
527
|
+
the same release reuses its deployment and rewrites only that deployment's secrets in place. Its
|
|
528
|
+
secrets file is `.env.admin`.
|
|
529
|
+
- **`admin-beta`**, frozen (`providers/cloudflare/operator/provision/legacy/admin-beta.ts`). It is
|
|
530
|
+
a Worker of its own, `services-domain-admin-beta` at `https://services-admin.beta.astrale.ai`,
|
|
531
|
+
that retains the issuer `https://services.beta.astrale.ai` and its signing key, as services-v2
|
|
532
|
+
does, so the Admin instance installs it without an issuer change (OPS-3). The cost: a Worker built
|
|
533
|
+
from main holds the key that 1Pact's installations trust. Keep the key only in managed secret
|
|
534
|
+
storage and in this Worker's `ASTRALE_SIGNING_IDENTITY` secret. It changes only for a same-Schema
|
|
535
|
+
fix the Admin instance needs before it moves; everything else goes to `admin`. It is deleted from
|
|
536
|
+
this repository by D19, once the Admin instance runs on `admin` and OPS-6 retired the Worker.
|
|
537
|
+
|
|
538
|
+
### Set up the frozen admin-beta runtime (OPS-3)
|
|
539
|
+
|
|
540
|
+
The operator sets it up once, from a commit of `main` that carries the Services CT16 adoption
|
|
541
|
+
(`bindings.dispatchNamespaces` and `settings.durableOptions` in the deployment configuration, with
|
|
542
|
+
the tenant refusal) and pins an SDK release that carries the CT16 extension (R6a' or later). An
|
|
543
|
+
admin-beta built without that adoption refuses every deploy of an adapter-astrale that carries it
|
|
544
|
+
with `1003 FUNCTION_INPUT_INVALID`, and the move to `admin` over the deployments such a runtime wrote
|
|
545
|
+
is refused with `4001 DATA_MIGRATION_REQUIRED` (see [OPS-5](#move-the-admin-instance-to-admin-ops-5)).
|
|
134
546
|
|
|
135
547
|
1. Read what the Admin instance's Services are bound to, read-only:
|
|
136
548
|
`astrale introspect services.astrale.ai -i <admin-instance> --json`. Record `revision` and
|
|
137
549
|
`publication.identity.issuer`, and the frozen runtime whose `/.well-known/astrale/domain.json`
|
|
138
550
|
declares that revision: it is the rollback URL. Go on only if the issuer is
|
|
139
|
-
`https://services.beta.astrale.ai`; any other issuer makes step 6 an issuer change, which
|
|
140
|
-
|
|
551
|
+
`https://services.beta.astrale.ai`; any other issuer makes step 6 an issuer change, which this
|
|
552
|
+
runtime does not take (use the [move to `admin`](#move-the-admin-instance-to-admin-ops-5)
|
|
553
|
+
instead). Check as well that the Admin instance's Kernel issuer is the
|
|
554
|
+
`SERVICES_DEPLOYMENT_KERNEL` var; otherwise every deploy answers `hosting-unavailable`. Before
|
|
555
|
+
the first readable deploy, read the platform organisation's Identity id there and set it as the
|
|
556
|
+
`SERVICES_PLATFORM_OWNER` var in a reviewed change.
|
|
141
557
|
2. Create `.env.admin-beta` (mode 600) from `.env.example` with the beta data-plane coordinates the
|
|
142
558
|
Admin instance's Services manage today (account, token, dispatch namespace, routing domain
|
|
143
559
|
`svc.eu.beta.astrale.ai`, schedule KV, dispatcher, zone, Admin target) and a new
|
|
144
|
-
`SERVICES_SECRET_DIGEST_KEY` that no other
|
|
145
|
-
random bytes, kept as their 44-character base64 text
|
|
560
|
+
`SERVICES_SECRET_DIGEST_KEY` that no other data plane uses, e.g. `openssl rand -base64 32` (32
|
|
561
|
+
random bytes, kept as their 44-character base64 text; the configuration refuses fewer than 32
|
|
562
|
+
UTF-8 bytes). It is generated once here, copied unchanged into `.env.admin` at OPS-5, and never
|
|
563
|
+
regenerated: the recorded secret digests of every hosted deployment depend on it.
|
|
146
564
|
3. Restore the retained `https://services.beta.astrale.ai` signing identity, the one services-v2 was
|
|
147
565
|
deployed with, from managed secret storage to `.astrale/services-beta-identity.json` (mode 600).
|
|
148
566
|
Never generate a key for this runtime: the frozen R1 Worker publishes the issuer's keys, so a new
|
|
149
|
-
key would never verify.
|
|
150
|
-
Environment's default identity: `pnpm dev` would bind it to development Workers. Check that the
|
|
151
|
-
issuer publishes the restored key:
|
|
567
|
+
key would never verify. Check that the issuer publishes the restored key:
|
|
152
568
|
|
|
153
569
|
```sh
|
|
154
570
|
node --input-type=module -e '
|
|
@@ -172,14 +588,149 @@ The operator sets it up once:
|
|
|
172
588
|
The install must succeed and the Service must stay manageable (list, secrets, logs). Then install
|
|
173
589
|
the step 1 rollback URL there again: if the Kernel refuses it, step 6 is one-way, and that must be
|
|
174
590
|
recorded before step 6 runs.
|
|
175
|
-
6. Install on the Admin instance only
|
|
176
|
-
`astrale domain install https://services-admin.beta.astrale.ai --
|
|
591
|
+
6. Install on the Admin instance only:
|
|
592
|
+
`astrale domain install https://services-admin.beta.astrale.ai --allow-identity-override -i <admin-instance>`.
|
|
177
593
|
The only consent is the identity override, because the origin `services.astrale.ai` differs from
|
|
178
594
|
the serving host; the issuer does not change. On any refusal, stop: never uninstall Services on
|
|
179
595
|
the Admin instance to get around it, which would drop the Service records it holds. Never repoint
|
|
180
596
|
Admin's catalog entry for `services.astrale.ai` at this runtime: consumer flows, 1Pact's among
|
|
181
597
|
them, install Services by origin through that entry.
|
|
182
598
|
|
|
599
|
+
### Move the Admin instance to admin (OPS-5)
|
|
600
|
+
|
|
601
|
+
OPS-5 is the production operation that moves the Admin instance's Services off the frozen
|
|
602
|
+
`admin-beta` Worker onto a deployment of `admin`. Before acting, write a compatibility record and
|
|
603
|
+
know the rollback path. Afterwards, verify the Admin instance and add an entry to the operations
|
|
604
|
+
log. 1Pact's instances and every consumer instance are out of scope: they keep the Services they
|
|
605
|
+
pin, and nothing here touches them.
|
|
606
|
+
|
|
607
|
+
Preconditions:
|
|
608
|
+
|
|
609
|
+
1. The Admin instance installs `admin-beta` (OPS-3), built with the Services CT16 adoption; its
|
|
610
|
+
rollback URL is `https://services-admin.beta.astrale.ai`.
|
|
611
|
+
2. The Admin instance's Host runs a Kernel that lists installed releases and takes issuer consent
|
|
612
|
+
(astrale-os/kernel#1124, in a released Host image, OPS-4). An older Kernel refuses
|
|
613
|
+
`--allow-issuer-change` before any install, so the move waits.
|
|
614
|
+
3. The operator's `astrale` CLI is a release with install by URL (astrale-os/cli#582) and issuer
|
|
615
|
+
consent (astrale-os/cli#583).
|
|
616
|
+
4. This package pins an SDK release whose adapter-cloudflare deploys namespace mode with dispatch
|
|
617
|
+
namespace bindings and durable options (astrale-os/sdk#599, on the CT16 extension of
|
|
618
|
+
astrale-os/sdk#587), and the publishers' adapter-astrale passes `dispatchNamespaces: []`
|
|
619
|
+
(astrale-os/sdk#598).
|
|
620
|
+
5. The platform namespace is provisioned (`pnpm provider:platform-bootstrap`, OPS-1) and the deploy
|
|
621
|
+
token may manage scripts in it.
|
|
622
|
+
|
|
623
|
+
Steps:
|
|
624
|
+
|
|
625
|
+
0. **Schema.** A move, and a rollback, is a Schema upgrade: the Kernel checks the data the Admin
|
|
626
|
+
instance stores against the Schema of the URL it installs. Read the installed `revision`
|
|
627
|
+
(`astrale introspect services.astrale.ai -i <admin-instance> --json`, as recorded at OPS-3
|
|
628
|
+
step 1) and, after step 2, the deployment's `schema.revision` in `services-admin.json`. Equal
|
|
629
|
+
revisions are the same Schema. Different revisions make the move a Schema upgrade: list what
|
|
630
|
+
changed in `services/schema/` between the OPS-3 commit and this one, and let the rehearsal
|
|
631
|
+
(step 3) decide. When admin-beta and `admin` are built from commits whose Services Schema is
|
|
632
|
+
the same (both with the CT16 adoption), only the compiled form can differ.
|
|
633
|
+
1. **Secrets.** Create `.env.admin` (mode 600) as a copy of `.env.admin-beta`. The values must be
|
|
634
|
+
the same, the `SERVICES_SECRET_DIGEST_KEY` above all: the new deployment manages the same hosted
|
|
635
|
+
deployments, in the same dispatch namespace and routing KV, and their recorded secret digests
|
|
636
|
+
depend on that key. No signing identity file: the deploy generates the deployment's key.
|
|
637
|
+
2. **Deploy.** From a clean checkout of `main`, with `CLOUDFLARE_ACCOUNT_ID` and
|
|
638
|
+
`CLOUDFLARE_API_TOKEN` of the platform account set, run
|
|
639
|
+
`pnpm run deploy admin --json > services-admin.json` (no `--`: pnpm passes it on literally and
|
|
640
|
+
`astrale-domain` refuses it as an unknown flag). Check that
|
|
641
|
+
`<url>/.well-known/astrale/release.json` serves the printed release digest and that
|
|
642
|
+
`<url>/.well-known/astrale/deployment.json` serves its record.
|
|
643
|
+
3. **Rehearse** on a disposable instance whose Host runs the Admin Host's Kernel release, whose
|
|
644
|
+
Services pin `admin-beta`, with at least one Service and one hosted deployment: move it (step 4),
|
|
645
|
+
verify it (step 5), roll it back ([Rollback](#rollback-ops-5)) and move it forward again.
|
|
646
|
+
4. **Move** the Admin instance:
|
|
647
|
+
|
|
648
|
+
```sh
|
|
649
|
+
astrale domain install "$(jq -r .url services-admin.json)" \
|
|
650
|
+
--allow-issuer-change=services.astrale.ai -i <admin-instance> --json
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
- The frozen Worker's issuer and the deployment's are not on one line, so consent names the
|
|
654
|
+
origin: a bare `--allow-issuer-change` covers only a new deployment of the same line, and the
|
|
655
|
+
install is refused without the origin.
|
|
656
|
+
- The replaced issuer keeps working while its in-flight work drains. Do not pass
|
|
657
|
+
`--revoke-previous`.
|
|
658
|
+
- If the Kernel refuses an installed dependent of Services, install Services together with a
|
|
659
|
+
compatible deployment of that dependent in one grouped command
|
|
660
|
+
(`astrale domain install <services-url> <dependent-url> …`); a refusal installs nothing.
|
|
661
|
+
- If the Kernel answers `4001 DATA_MIGRATION_REQUIRED` ("Schema change requires an explicit
|
|
662
|
+
data migration."), data the Admin instance stores does not fit the deployment's Schema, and
|
|
663
|
+
`reason.details.requirements` names each Class (an admin-beta built without the CT16 adoption
|
|
664
|
+
names `CloudflareDeployment` and `deployment_on_line`). The whole operation is refused: the
|
|
665
|
+
pin does not move, nothing activates and no data changes, although the deployment did receive
|
|
666
|
+
the install request. The Admin instance stays on admin-beta, which keeps answering. Fix
|
|
667
|
+
forward with an `admin` release whose Schema admits the stored data; this repository has no
|
|
668
|
+
data migration. `SCHEMA_UPGRADE_INCOMPATIBLE` (4002) and `SCHEMA_DEPENDENTS_INCOMPATIBLE`
|
|
669
|
+
have the same remedy.
|
|
670
|
+
- On any other refusal, stop. Never uninstall Services on the Admin instance.
|
|
671
|
+
5. **Verify**, with reads only, then one hosted deploy:
|
|
672
|
+
- `astrale introspect services.astrale.ai -i <admin-instance> --json` names the new release,
|
|
673
|
+
pinned exactly, with the deployment URL as issuer;
|
|
674
|
+
- `astrale call /:services.astrale.ai:class.CloudflareDeployment:hosting -i <admin-instance> --json`
|
|
675
|
+
answers the hosting of `svc.eu.beta.astrale.ai`;
|
|
676
|
+
- the Services View lists the Admin instance's Services and their secrets names;
|
|
677
|
+
- a publisher's `astrale-domain deploy <environment>` of a Domain with the astrale adapter into
|
|
678
|
+
the Admin instance still makes, or reuses, its hosted deployment.
|
|
679
|
+
6. **Point bootstrap discovery at the deployment.** Admin's bootstrap discovery for `services`
|
|
680
|
+
moves to this deployment's `release.json` (A11), and Admin is redeployed (OPS-7). Never repoint
|
|
681
|
+
Admin's catalog entry for `services.astrale.ai`: consumer flows install Services by origin
|
|
682
|
+
through it.
|
|
683
|
+
|
|
684
|
+
A later release moves the same way: deploy `admin`, then install its URL, with the step 0 Schema
|
|
685
|
+
check. Its issuer is another deployment URL on the same line, which the bare `--allow-issuer-change`
|
|
686
|
+
covers by the consent rule (CT24); the host proof does not exercise that case, so pass
|
|
687
|
+
`--allow-issuer-change=services.astrale.ai` if the CLI asks for the origin.
|
|
688
|
+
|
|
689
|
+
#### Rollback (OPS-5)
|
|
690
|
+
|
|
691
|
+
Reinstall the frozen Worker with consent. It still serves, because only OPS-6 retires it:
|
|
692
|
+
|
|
693
|
+
```sh
|
|
694
|
+
astrale domain install https://services-admin.beta.astrale.ai \
|
|
695
|
+
--allow-issuer-change=services.astrale.ai --allow-identity-override -i <admin-instance> --json
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
It serves only the legacy `domain.json`, and the origin it declares differs from its host, so the
|
|
699
|
+
identity override applies as well. The Admin instance's Services records stay in its Kernel through
|
|
700
|
+
a move and a rollback, and a rollback is a Schema upgrade too: it is admitted only while
|
|
701
|
+
admin-beta's Schema admits the data written since the move. Once it does not, the Kernel refuses
|
|
702
|
+
as in step 4 and the deployment stays installed; the remedy is then a new `admin` deployment that
|
|
703
|
+
fixes forward, never a redeploy of admin-beta. If step 6 already repointed bootstrap discovery,
|
|
704
|
+
repoint it back too.
|
|
705
|
+
|
|
706
|
+
### Retire admin-beta (OPS-6)
|
|
707
|
+
|
|
708
|
+
Once the Admin instance runs on `admin` and no installation on any Host pins
|
|
709
|
+
`https://services-admin.beta.astrale.ai` (`astrale introspect services.astrale.ai` on each instance
|
|
710
|
+
that installs Services), retire the `services-domain-admin-beta` Worker and its route
|
|
711
|
+
`services-admin.beta.astrale.ai`, and delete its `ASTRALE_SIGNING_IDENTITY` secret, its copy of the
|
|
712
|
+
services.beta key. Keep the issuer host `services.beta.astrale.ai` and the key itself: the frozen
|
|
713
|
+
R1 and services-v2 Workers that 1Pact pins still sign with them. Then D19 deletes the frozen
|
|
714
|
+
`admin-beta` Environment from this repository.
|
|
715
|
+
|
|
716
|
+
### Before publishers deploy
|
|
717
|
+
|
|
718
|
+
The Admin instance's graph is the registry of lines. Before the first publisher deploys:
|
|
719
|
+
|
|
720
|
+
1. This runtime is installed on the Admin instance only, as above; that instance's Kernel issuer is
|
|
721
|
+
`SERVICES_DEPLOYMENT_KERNEL`; and `SERVICES_PLATFORM_OWNER` names the platform organisation before
|
|
722
|
+
its first readable deploy.
|
|
723
|
+
2. Each publisher has an organisation on the Admin instance: an Identity, a Shell User for a single
|
|
724
|
+
publisher or a Shell Group otherwise. The first deploy of an origin in an environment gives that
|
|
725
|
+
line to the organisation it names, and only a [transfer](#transfer-a-line) moves it.
|
|
726
|
+
3. Its members and CI identities are Identities of the Admin instance that act for it through
|
|
727
|
+
Kernel `extends_with`. A Shell Group's `assignMember` writes that Edge for each User or Group it
|
|
728
|
+
assigns. A caller that does not act for the organisation it names is refused before any write.
|
|
729
|
+
4. The Admin instance's graph is backed up with the registry it holds: only it records who holds
|
|
730
|
+
each line, each deployment's record and its secret digests. A restore loses what happened after
|
|
731
|
+
its backup: lines registered since are free for the next organisation that deploys, and secret
|
|
732
|
+
digests may name values older than the ones served.
|
|
733
|
+
|
|
183
734
|
## Plain background Service starter
|
|
184
735
|
|
|
185
736
|
The package also ships an isolated scaffolding binary (the root import remains Schema-only):
|
|
@@ -202,12 +753,13 @@ No tooling dependency is added to the Schema runtime or imported from the packag
|
|
|
202
753
|
|
|
203
754
|
## Domain development
|
|
204
755
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
756
|
+
Services has no development Environment: the [Admin-hosted runtime](#admin-hosted-runtime) is the
|
|
757
|
+
only one it hosts, and Services itself is developed against its tests (the fake Cloudflare account
|
|
758
|
+
and the [real-Kernel suite](tests/kernel/README.md)). `pnpm run deploy admin` makes an immutable
|
|
759
|
+
deployment of this release on the `admin` line of the platform namespace. Deploying never
|
|
760
|
+
installs: installation is always a separate, explicit `astrale domain install <url> -i <instance>`
|
|
761
|
+
on the intended Kernel. There is no watch mode: `astrale-domain dev` serves only direct-mode
|
|
762
|
+
Environments.
|
|
211
763
|
|
|
212
764
|
To validate without deploying:
|
|
213
765
|
|
|
@@ -250,12 +802,12 @@ candidate and the serving Worker. Those exact flags participate in the deploymen
|
|
|
250
802
|
Services must not inject defaults after its computation. The Cloudflare adapter already includes
|
|
251
803
|
public-fetch routing in the generated configuration before building the artifact and its digest.
|
|
252
804
|
|
|
253
|
-
SDK-authored
|
|
805
|
+
SDK-authored Domains should deploy through `@astrale-os/adapter-astrale`, which projects the
|
|
254
806
|
admitted Cloudflare artifact into the revisioned Services request and verifies the live Publication.
|
|
255
|
-
|
|
256
|
-
Services never interprets or
|
|
257
|
-
|
|
258
|
-
deploy provider compute first and then
|
|
807
|
+
Installation is never part of a deploy: install the deployed URL with
|
|
808
|
+
`astrale domain install <url> -i <instance>` as its own step. Services never interprets or caches
|
|
809
|
+
a Domain definition, Publication, Schema revision, issuer, or callable inventory. Manual callers likewise
|
|
810
|
+
deploy provider compute first and then install with the same command.
|
|
259
811
|
|
|
260
812
|
Revisioned deployments may request `readiness: { paths: ['/health'] }`. Services executes these
|
|
261
813
|
bounded local GET paths through its private candidate dispatch binding and requires HTTP 200 before
|