@astrale-domains/services 0.4.7 → 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.
Files changed (2) hide show
  1. package/README.md +438 -43
  2. package/package.json +3 -4
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.
@@ -123,7 +123,7 @@ the routing KV are shared. Any other Kernel finds hosting unavailable before any
123
123
  Only a runtime whose Environment vars set `SERVICES_DEPLOYMENT_HOST` (the deployments' routing
124
124
  domain) hosts deployments; every other runtime answers `hosting-unavailable`. Such a runtime refuses
125
125
  to boot without `SERVICES_DEPLOYMENT_KERNEL` (the Admin Kernel's issuer URL),
126
- `SERVICES_SECRET_DIGEST_KEY` (at least 32 characters, kept for the life of the environment: the
126
+ `SERVICES_SECRET_DIGEST_KEY` (at least 32 UTF-8 bytes, kept for the life of the data plane: the
127
127
  recorded digests depend on it), the `SERVICE_DEPLOYMENTS` dispatch binding to the environment's
128
128
  deployments namespace, and `CLOUDFLARE_SCHEDULE_KV_ID`, the routing KV that activates a label, and
129
129
  refuses a `SERVICES_PLATFORM_OWNER` that is not an Identity id. Hosting is declared once, by the
@@ -189,7 +189,8 @@ release digest at the same URL. A `withdrawn` tombstone is never removed.
189
189
  `SERVICES_REGISTRY_ORIGIN` and `SERVICES_PREVIEW_EXPIRY` (`plan`, the default, or `enforce`) are
190
190
  Environment vars of a hosting runtime, refused in a secrets file and refused without
191
191
  `SERVICES_DEPLOYMENT_HOST`. Without a registry origin no caller is the registry, and `retain` and
192
- `expirePreviews` are refused. `admin-beta` names `admin.astrale.ai` and keeps expiry a plan.
192
+ `expirePreviews` are refused. `admin` and the frozen `admin-beta` name `admin.astrale.ai` and keep
193
+ expiry a plan.
193
194
  Reading last calls needs the API token's Account Analytics Read permission. `pnpm
194
195
  provider:deployment-audit` lists the deployments namespace read-only
195
196
  ([operator](providers/cloudflare/operator/README.md)).
@@ -206,6 +207,246 @@ applies `OwnService` to each one. Workflow addresses are `CloudflareWorker.*`; a
206
207
  keys are not executable. A future provider Class must supply its own concrete declarations and
207
208
  implementations. Properties and ownership relationships retain their inherited meaning.
208
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
+
209
450
  ## Shared public ingress
210
451
 
211
452
  New Services use the environment's proxied wildcard DNS, wildcard Worker route and wildcard
@@ -257,15 +498,15 @@ data.
257
498
 
258
499
  ## Admin-hosted runtime
259
500
 
260
- The `admin-beta` Environment is the Services runtime of the Admin instance, the one that hosts
261
- publishers' deployments outside the instances that install them. It is a Worker of its own,
262
- `services-domain-admin-beta` at `https://services-admin.beta.astrale.ai`, on the beta data plane the
263
- Admin instance's Services already manage. Its plain vars name the router Worker
264
- (`ASTRALE_ROUTER_WORKER`), the routing domain under which deployments are addressed
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
265
506
  (`SERVICES_DEPLOYMENT_HOST`, served by the Services dispatcher), the Admin instance's Kernel, the
266
507
  only one it hosts deployments for (`SERVICES_DEPLOYMENT_KERNEL`, `https://admin.eu.beta.astrale.ai/api`),
267
508
  and the registry Domain (`SERVICES_REGISTRY_ORIGIN`). These vars are the only declaration of
268
- hosting: they also bind `SERVICE_DEPLOYMENTS`. `.env.admin-beta` must set
509
+ hosting: they also bind `SERVICE_DEPLOYMENTS`. Its secrets file must set
269
510
  `SERVICES_SECRET_DIGEST_KEY`, the key of each hosted deployment's secret digest, and the dispatch
270
511
  namespace and routing KV the runtime boots with. The runtime keys HMAC-SHA256 with the UTF-8 bytes
271
512
  of the digest key as written, never base64-decoded, so the configuration refuses, before any
@@ -274,38 +515,56 @@ sets a hosting var. `SERVICES_PLATFORM_OWNER` is not set yet: until it names the
274
515
  organisation's Identity id on the Admin instance, every line is opaque and a readable deploy is
275
516
  refused.
276
517
 
277
- It retains the issuer `https://services.beta.astrale.ai` and its signing key, as services-v2 does.
278
- The Admin instance's Services are bound to that issuer, and the Kernel refuses an issuer change when
279
- it replaces an installed Domain (`issuer-changed`); neither the Admin Host's Kernel nor the CLI
280
- offers a consented issuer change yet. The cost: a Worker built from main holds the key that 1Pact's
281
- installations trust. Keep the key only in managed secret storage and in this Worker's
282
- `ASTRALE_SIGNING_IDENTITY` secret. The Admin instance moves to an issuer of its own when the
283
- canonical Admin-hosted runtime replaces this one through a consented issuer change; this Worker and
284
- its copy of the key are retired after that reinstall.
285
-
286
- Only the Admin instance installs this runtime; consumer instances keep the Services they pin.
287
- The operator sets it up once:
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)).
288
546
 
289
547
  1. Read what the Admin instance's Services are bound to, read-only:
290
548
  `astrale introspect services.astrale.ai -i <admin-instance> --json`. Record `revision` and
291
549
  `publication.identity.issuer`, and the frozen runtime whose `/.well-known/astrale/domain.json`
292
550
  declares that revision: it is the rollback URL. Go on only if the issuer is
293
- `https://services.beta.astrale.ai`; any other issuer makes step 6 an issuer change, which the
294
- Kernel refuses. Check as well that the Admin instance's Kernel issuer is admin-beta's
295
- `SERVICES_DEPLOYMENT_KERNEL`; otherwise every deploy answers `hosting-unavailable`. Before the
296
- first readable deploy, read the platform organisation's Identity id there and set it as
297
- admin-beta's `SERVICES_PLATFORM_OWNER` var in a reviewed change to `astrale.config.ts`.
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.
298
557
  2. Create `.env.admin-beta` (mode 600) from `.env.example` with the beta data-plane coordinates the
299
558
  Admin instance's Services manage today (account, token, dispatch namespace, routing domain
300
559
  `svc.eu.beta.astrale.ai`, schedule KV, dispatcher, zone, Admin target) and a new
301
- `SERVICES_SECRET_DIGEST_KEY` that no other environment uses, e.g. `openssl rand -base64 32` (32
302
- 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.
303
564
  3. Restore the retained `https://services.beta.astrale.ai` signing identity, the one services-v2 was
304
565
  deployed with, from managed secret storage to `.astrale/services-beta-identity.json` (mode 600).
305
566
  Never generate a key for this runtime: the frozen R1 Worker publishes the issuer's keys, so a new
306
- key would never verify. Never leave that key at `.astrale/identity.json`, the development
307
- Environment's default identity: `pnpm dev` would bind it to development Workers. Check that the
308
- issuer publishes the restored key:
567
+ key would never verify. Check that the issuer publishes the restored key:
309
568
 
310
569
  ```sh
311
570
  node --input-type=module -e '
@@ -329,14 +588,149 @@ The operator sets it up once:
329
588
  The install must succeed and the Service must stay manageable (list, secrets, logs). Then install
330
589
  the step 1 rollback URL there again: if the Kernel refuses it, step 6 is one-way, and that must be
331
590
  recorded before step 6 runs.
332
- 6. Install on the Admin instance only, directly:
333
- `astrale domain install https://services-admin.beta.astrale.ai --direct --allow-identity-override -i <admin-instance>`.
591
+ 6. Install on the Admin instance only:
592
+ `astrale domain install https://services-admin.beta.astrale.ai --allow-identity-override -i <admin-instance>`.
334
593
  The only consent is the identity override, because the origin `services.astrale.ai` differs from
335
594
  the serving host; the issuer does not change. On any refusal, stop: never uninstall Services on
336
595
  the Admin instance to get around it, which would drop the Service records it holds. Never repoint
337
596
  Admin's catalog entry for `services.astrale.ai` at this runtime: consumer flows, 1Pact's among
338
597
  them, install Services by origin through that entry.
339
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
+
340
734
  ## Plain background Service starter
341
735
 
342
736
  The package also ships an isolated scaffolding binary (the root import remains Schema-only):
@@ -359,12 +753,13 @@ No tooling dependency is added to the Schema runtime or imported from the packag
359
753
 
360
754
  ## Domain development
361
755
 
362
- Copy `.env.example` to `.env.dev` and fill the Cloudflare values.
363
-
364
- `pnpm dev` watches and deploys the development Environment remotely. `pnpm run deploy admin-beta`
365
- deploys the [Admin-hosted runtime](#admin-hosted-runtime) once. These bootstrap Environments
366
- are deliberately deploy-only: install the resulting URL on the intended Kernel explicitly.
367
- Stopping development leaves the remote Worker available. No local Worker or tunnel is started.
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.
368
763
 
369
764
  To validate without deploying:
370
765
 
@@ -407,12 +802,12 @@ candidate and the serving Worker. Those exact flags participate in the deploymen
407
802
  Services must not inject defaults after its computation. The Cloudflare adapter already includes
408
803
  public-fetch routing in the generated configuration before building the artifact and its digest.
409
804
 
410
- SDK-authored Applications should deploy through `@astrale-os/adapter-astrale`, which projects the
805
+ SDK-authored Domains should deploy through `@astrale-os/adapter-astrale`, which projects the
411
806
  admitted Cloudflare artifact into the revisioned Services request and verifies the live Publication.
412
- Project tooling installs the exact Release only when that environment declares installation.
413
- Services never interprets or
414
- caches Application, Publication, Schema revision, issuer, or callable inventory. Manual callers may
415
- deploy provider compute first and then use the ordinary Domain installation command themselves.
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.
416
811
 
417
812
  Revisioned deployments may request `readiness: { paths: ['/health'] }`. Services executes these
418
813
  bounded local GET paths through its private candidate dispatch binding and requires HTTP 200 before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-domains/services",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/astrale-os/domains.git",
@@ -31,11 +31,11 @@
31
31
  "registry": "https://registry.npmjs.org/"
32
32
  },
33
33
  "dependencies": {
34
- "@astrale-os/sdk": "0.6.0-beta.13",
34
+ "@astrale-os/sdk": "0.6.0-beta.14",
35
35
  "zod": "4.4.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@astrale-os/adapter-cloudflare": "0.5.0-beta.168",
38
+ "@astrale-os/adapter-cloudflare": "0.5.0-beta.169",
39
39
  "@astrale-os/ox": ">=0.1.4 <1.0.0",
40
40
  "@astrale-os/shell": "0.6.0-beta.8",
41
41
  "@astrale-os/shell-react": "0.5.0-beta.8",
@@ -71,7 +71,6 @@
71
71
  "workerd"
72
72
  ],
73
73
  "scripts": {
74
- "dev": "astrale-domain dev",
75
74
  "deploy": "astrale-domain deploy",
76
75
  "build": "astrale-domain build",
77
76
  "lint": "astrale-domain lint",