@astrale-domains/services 0.4.9 → 0.5.0

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 (181) hide show
  1. package/README.md +153 -816
  2. package/dist/schema/index.d.ts +4 -5
  3. package/dist/schema/index.js +2 -3
  4. package/dist/schema/modules/deployment/classes/cloudflare-deployment.d.ts +4 -321
  5. package/dist/schema/modules/deployment/classes/cloudflare-deployment.js +1 -19
  6. package/dist/schema/modules/deployment/classes/current-deployment.d.ts +20 -0
  7. package/dist/schema/modules/deployment/classes/current-deployment.js +13 -0
  8. package/dist/schema/modules/deployment/classes/{deployment-on-line.d.ts → deployment-of.d.ts} +3 -3
  9. package/dist/schema/modules/deployment/classes/deployment-of.js +11 -0
  10. package/dist/schema/modules/deployment/classes/deployment.d.ts +9 -57
  11. package/dist/schema/modules/deployment/classes/deployment.js +9 -23
  12. package/dist/schema/modules/deployment/classes/index.d.ts +2 -3
  13. package/dist/schema/modules/deployment/classes/index.js +2 -3
  14. package/dist/schema/modules/deployment/errors/deployment-not-found.d.ts +2 -5
  15. package/dist/schema/modules/deployment/errors/deployment-not-found.js +2 -5
  16. package/dist/schema/modules/deployment/errors/index.d.ts +0 -2
  17. package/dist/schema/modules/deployment/errors/index.js +0 -2
  18. package/dist/schema/modules/deployment/functions/index.d.ts +0 -10
  19. package/dist/schema/modules/deployment/functions/index.js +0 -10
  20. package/dist/schema/modules/deployment/functions/retire-deployment.d.ts +8 -7
  21. package/dist/schema/modules/deployment/functions/retire-deployment.js +8 -7
  22. package/dist/schema/modules/deployment/index.d.ts +6 -6
  23. package/dist/schema/modules/deployment/index.js +7 -5
  24. package/dist/schema/modules/deployment/policies/index.d.ts +1 -4
  25. package/dist/schema/modules/deployment/policies/index.js +1 -4
  26. package/dist/schema/modules/deployment/policies/own-deployment.d.ts +1 -1
  27. package/dist/schema/modules/deployment/policies/own-deployment.js +6 -7
  28. package/dist/schema/modules/deployment/policies/traverse-owned-service-deployment.d.ts +2 -0
  29. package/dist/schema/modules/deployment/policies/traverse-owned-service-deployment.js +7 -0
  30. package/dist/schema/modules/deployment/states/deployment-lifecycle.d.ts +6 -9
  31. package/dist/schema/modules/deployment/states/deployment-lifecycle.js +6 -7
  32. package/dist/schema/modules/deployment/types/deployment-hosting.d.ts +9 -28
  33. package/dist/schema/modules/deployment/types/deployment-hosting.js +3 -3
  34. package/dist/schema/modules/deployment/types/deployment-request.d.ts +12 -40
  35. package/dist/schema/modules/deployment/types/deployment-request.js +17 -9
  36. package/dist/schema/modules/deployment/types/deployment-response.d.ts +7 -15
  37. package/dist/schema/modules/deployment/types/deployment-response.js +2 -2
  38. package/dist/schema/modules/deployment/types/deployment-retire.d.ts +1 -1
  39. package/dist/schema/modules/deployment/types/deployment-secret.d.ts +0 -39
  40. package/dist/schema/modules/deployment/types/deployment-secret.js +0 -34
  41. package/dist/schema/modules/deployment/types/deployment-summary.d.ts +14 -45
  42. package/dist/schema/modules/deployment/types/deployment-summary.js +9 -8
  43. package/dist/schema/modules/deployment/types/index.d.ts +7 -3
  44. package/dist/schema/modules/deployment/types/index.js +4 -2
  45. package/dist/schema/modules/{service → deployment}/types/service-deployment-artifact-module.d.ts +2 -20
  46. package/dist/schema/modules/deployment/types/service-deployment-artifact-module.js +10 -0
  47. package/dist/schema/modules/{service → deployment}/types/service-deployment-assets.d.ts +4 -20
  48. package/dist/schema/modules/{service → deployment}/types/service-deployment-assets.js +4 -7
  49. package/dist/schema/modules/service/classes/cloudflare-service.d.ts +321 -0
  50. package/dist/schema/modules/service/classes/cloudflare-service.js +35 -0
  51. package/dist/schema/modules/service/classes/index.d.ts +1 -1
  52. package/dist/schema/modules/service/classes/index.js +1 -1
  53. package/dist/schema/modules/service/classes/service.d.ts +18 -107
  54. package/dist/schema/modules/service/classes/service.js +16 -27
  55. package/dist/schema/modules/service/errors/index.d.ts +1 -0
  56. package/dist/schema/modules/service/errors/index.js +1 -0
  57. package/dist/schema/modules/service/errors/service-not-found.d.ts +7 -0
  58. package/dist/schema/modules/service/errors/service-not-found.js +8 -0
  59. package/dist/schema/modules/service/functions/delete-service.d.ts +1 -1
  60. package/dist/schema/modules/service/functions/delete-service.js +1 -1
  61. package/dist/schema/modules/{deployment/functions/deploy-cloudflare-deployment.d.ts → service/functions/deploy-service.d.ts} +20 -35
  62. package/dist/schema/modules/service/functions/deploy-service.js +15 -0
  63. package/dist/schema/modules/service/functions/describe-hosting.d.ts +8 -0
  64. package/dist/schema/modules/service/functions/describe-hosting.js +11 -0
  65. package/dist/schema/modules/service/functions/index.d.ts +5 -10
  66. package/dist/schema/modules/service/functions/index.js +5 -10
  67. package/dist/schema/modules/{deployment/functions/list-cloudflare-deployments.d.ts → service/functions/list-service-deployments.d.ts} +8 -12
  68. package/dist/schema/modules/service/functions/list-service-deployments.js +10 -0
  69. package/dist/schema/modules/service/functions/list-service-logs.d.ts +2 -1
  70. package/dist/schema/modules/service/functions/list-service-logs.js +1 -1
  71. package/dist/schema/modules/service/functions/list-service-schedules.d.ts +1 -1
  72. package/dist/schema/modules/service/functions/list-service-schedules.js +1 -1
  73. package/dist/schema/modules/service/functions/list-service-secret-names.d.ts +1 -1
  74. package/dist/schema/modules/service/functions/list-service-secret-names.js +1 -1
  75. package/dist/schema/modules/service/functions/promote-service.d.ts +14 -0
  76. package/dist/schema/modules/service/functions/promote-service.js +4 -0
  77. package/dist/schema/modules/service/functions/replace-service-schedules.d.ts +1 -1
  78. package/dist/schema/modules/service/functions/replace-service-schedules.js +1 -4
  79. package/dist/schema/modules/service/functions/set-service-secret.d.ts +4 -2
  80. package/dist/schema/modules/service/functions/set-service-secret.js +1 -1
  81. package/dist/schema/modules/service/functions/transfer-service.d.ts +14 -0
  82. package/dist/schema/modules/service/functions/transfer-service.js +4 -0
  83. package/dist/schema/modules/service/index.d.ts +5 -5
  84. package/dist/schema/modules/service/index.js +5 -4
  85. package/dist/schema/modules/service/policies/act-as.d.ts +6 -0
  86. package/dist/schema/modules/service/policies/act-as.js +10 -0
  87. package/dist/schema/modules/service/policies/index.d.ts +1 -0
  88. package/dist/schema/modules/service/policies/index.js +1 -0
  89. package/dist/schema/modules/service/states/service-lifecycle.d.ts +7 -20
  90. package/dist/schema/modules/service/states/service-lifecycle.js +6 -21
  91. package/dist/schema/modules/service/types/index.d.ts +1 -13
  92. package/dist/schema/modules/service/types/index.js +1 -8
  93. package/dist/schema/modules/service/types/service-management.d.ts +65 -24
  94. package/dist/schema/modules/service/types/service-management.js +46 -21
  95. package/dist/schema/schema.d.ts +252 -758
  96. package/dist/schema/schema.js +7 -11
  97. package/package.json +2 -9
  98. package/dist/scaffold/create-service.mjs +0 -77
  99. package/dist/scaffold/template/.env.example +0 -2
  100. package/dist/scaffold/template/README.md +0 -37
  101. package/dist/scaffold/template/gitignore +0 -6
  102. package/dist/scaffold/template/scripts/build.mjs +0 -10
  103. package/dist/scaffold/template/scripts/deploy.mjs +0 -50
  104. package/dist/scaffold/template/src/account.js +0 -26
  105. package/dist/scaffold/template/src/index.js +0 -32
  106. package/dist/scaffold/template/src/job.js +0 -19
  107. package/dist/schema/modules/deployment/classes/deployment-line.d.ts +0 -23
  108. package/dist/schema/modules/deployment/classes/deployment-line.js +0 -24
  109. package/dist/schema/modules/deployment/classes/deployment-on-line.js +0 -11
  110. package/dist/schema/modules/deployment/classes/line-owned-by.d.ts +0 -26
  111. package/dist/schema/modules/deployment/classes/line-owned-by.js +0 -19
  112. package/dist/schema/modules/deployment/errors/deployment-label-not-found.d.ts +0 -12
  113. package/dist/schema/modules/deployment/errors/deployment-label-not-found.js +0 -11
  114. package/dist/schema/modules/deployment/errors/deployment-registry-only.d.ts +0 -11
  115. package/dist/schema/modules/deployment/errors/deployment-registry-only.js +0 -12
  116. package/dist/schema/modules/deployment/functions/deploy-cloudflare-deployment.js +0 -14
  117. package/dist/schema/modules/deployment/functions/describe-deployment-hosting.d.ts +0 -8
  118. package/dist/schema/modules/deployment/functions/describe-deployment-hosting.js +0 -11
  119. package/dist/schema/modules/deployment/functions/expire-deployment-previews.d.ts +0 -16
  120. package/dist/schema/modules/deployment/functions/expire-deployment-previews.js +0 -14
  121. package/dist/schema/modules/deployment/functions/list-cloudflare-deployment-secret-names.d.ts +0 -5
  122. package/dist/schema/modules/deployment/functions/list-cloudflare-deployment-secret-names.js +0 -4
  123. package/dist/schema/modules/deployment/functions/list-cloudflare-deployments.js +0 -10
  124. package/dist/schema/modules/deployment/functions/list-deployment-secret-names.d.ts +0 -4
  125. package/dist/schema/modules/deployment/functions/list-deployment-secret-names.js +0 -6
  126. package/dist/schema/modules/deployment/functions/read-deployment-last-calls.d.ts +0 -10
  127. package/dist/schema/modules/deployment/functions/read-deployment-last-calls.js +0 -13
  128. package/dist/schema/modules/deployment/functions/retain-deployment.d.ts +0 -12
  129. package/dist/schema/modules/deployment/functions/retain-deployment.js +0 -13
  130. package/dist/schema/modules/deployment/functions/set-cloudflare-deployment-secret.d.ts +0 -18
  131. package/dist/schema/modules/deployment/functions/set-cloudflare-deployment-secret.js +0 -7
  132. package/dist/schema/modules/deployment/functions/set-deployment-secret.d.ts +0 -14
  133. package/dist/schema/modules/deployment/functions/set-deployment-secret.js +0 -3
  134. package/dist/schema/modules/deployment/policies/act-for-organization.d.ts +0 -6
  135. package/dist/schema/modules/deployment/policies/act-for-organization.js +0 -10
  136. package/dist/schema/modules/deployment/policies/own-line.d.ts +0 -5
  137. package/dist/schema/modules/deployment/policies/own-line.js +0 -10
  138. package/dist/schema/modules/deployment/policies/traverse-owned-line-deployment.d.ts +0 -2
  139. package/dist/schema/modules/deployment/policies/traverse-owned-line-deployment.js +0 -7
  140. package/dist/schema/modules/deployment/policies/traverse-owned-line.d.ts +0 -2
  141. package/dist/schema/modules/deployment/policies/traverse-owned-line.js +0 -7
  142. package/dist/schema/modules/deployment/types/deployment-retention.d.ts +0 -49
  143. package/dist/schema/modules/deployment/types/deployment-retention.js +0 -45
  144. package/dist/schema/modules/service/classes/cloudflare-worker.d.ts +0 -352
  145. package/dist/schema/modules/service/classes/cloudflare-worker.js +0 -41
  146. package/dist/schema/modules/service/functions/delete-service-secret.d.ts +0 -11
  147. package/dist/schema/modules/service/functions/delete-service-secret.js +0 -3
  148. package/dist/schema/modules/service/functions/delete-worker-secret.d.ts +0 -11
  149. package/dist/schema/modules/service/functions/delete-worker-secret.js +0 -7
  150. package/dist/schema/modules/service/functions/delete-worker.d.ts +0 -9
  151. package/dist/schema/modules/service/functions/delete-worker.js +0 -7
  152. package/dist/schema/modules/service/functions/deploy-cloudflare-worker.d.ts +0 -109
  153. package/dist/schema/modules/service/functions/deploy-cloudflare-worker.js +0 -9
  154. package/dist/schema/modules/service/functions/list-worker-logs.d.ts +0 -12
  155. package/dist/schema/modules/service/functions/list-worker-logs.js +0 -7
  156. package/dist/schema/modules/service/functions/list-worker-schedules.d.ts +0 -4
  157. package/dist/schema/modules/service/functions/list-worker-schedules.js +0 -7
  158. package/dist/schema/modules/service/functions/list-worker-secret-names.d.ts +0 -4
  159. package/dist/schema/modules/service/functions/list-worker-secret-names.js +0 -7
  160. package/dist/schema/modules/service/functions/migrate-worker-ingress.d.ts +0 -24
  161. package/dist/schema/modules/service/functions/migrate-worker-ingress.js +0 -26
  162. package/dist/schema/modules/service/functions/replace-worker-schedules.d.ts +0 -11
  163. package/dist/schema/modules/service/functions/replace-worker-schedules.js +0 -7
  164. package/dist/schema/modules/service/functions/set-worker-secret.d.ts +0 -12
  165. package/dist/schema/modules/service/functions/set-worker-secret.js +0 -7
  166. package/dist/schema/modules/service/types/deploy-cloudflare-worker.d.ts +0 -286
  167. package/dist/schema/modules/service/types/deploy-cloudflare-worker.js +0 -51
  168. package/dist/schema/modules/service/types/legacy/service-deployment-content.js +0 -22
  169. package/dist/schema/modules/service/types/service-deployment-artifact-module.js +0 -13
  170. package/dist/schema/modules/service/types/service-deployment-resource-limits.d.ts +0 -6
  171. package/dist/schema/modules/service/types/service-deployment-resource-limits.js +0 -7
  172. package/dist/schema/modules/service/types/service-readiness.d.ts +0 -6
  173. package/dist/schema/modules/service/types/service-readiness.js +0 -6
  174. package/dist/schema/modules/service/types/service-secret-operation-rejection.d.ts +0 -9
  175. package/dist/schema/modules/service/types/service-secret-operation-rejection.js +0 -5
  176. /package/dist/schema/modules/{service → deployment}/types/content/index.d.ts +0 -0
  177. /package/dist/schema/modules/{service → deployment}/types/content/index.js +0 -0
  178. /package/dist/schema/modules/{service → deployment}/types/content/limits.d.ts +0 -0
  179. /package/dist/schema/modules/{service → deployment}/types/content/limits.js +0 -0
  180. /package/dist/schema/modules/{service → deployment}/types/service-deployment-content.d.ts +0 -0
  181. /package/dist/schema/modules/{service → deployment}/types/service-deployment-content.js +0 -0
package/README.md CHANGED
@@ -1,767 +1,186 @@
1
1
  # Services
2
2
 
3
- `services.astrale.ai` owns provider-backed compute resources. It keeps provider
4
- protocols behind neutral SDK Integrations and stores only admitted lifecycle evidence in the graph.
3
+ `services.astrale.ai` hosts Services: provider-backed compute an Identity owns. A Service is the
4
+ stable identity of one origin in one environment. Every deploy adds an immutable deployment of it,
5
+ and the Service's URL serves the one it currently points at, like a project and its deployments.
6
+ Provider protocols stay behind a neutral SDK Integration, and the graph keeps only admitted
7
+ lifecycle evidence.
5
8
 
6
9
  ## Model
7
10
 
8
- - `Service` records one opaque provider name, its owner-scoped stable `serviceKey`, deployment mode,
9
- URL, lifecycle, provider digest, operation receipt, and exact cleanup locators.
10
- - `service_owned_by` connects each Service to its owning Identity and governs graph visibility.
11
- - Owners may be Shell users, local Kernel roots, or application/CI identities. Services requires only
12
- the Kernel Schema; a Shell installation is not required to deploy. Ownership
13
- remains exact-subject scoped, and existing Shell-user ownership edges remain valid.
14
- - a `DeploymentLine` is one `(origin, environment)`, named by its SDK fingerprint. `line_owned_by`
15
- gives it to at most one organisation Identity, first come. Members and CI identities read the
16
- line and its deployments through Kernel `extends_with` (`OwnLine`, `OwnDeployment`). A line is
17
- never reassigned. Deleting its deployments, the organisation's Services or the organisation
18
- itself leaves the line refused to every other organisation (`line-owned-elsewhere`). Only an
19
- operator transfer moves the holder Edge.
20
- - the line Queries, `admitDeploymentLine` and the deployment Mutations run under the Services
21
- Domain credential and trust the organisation they are given. Under a caller's credential the
22
- Policies hide a foreign line, which would read as a free one. Their caller must first prove that
23
- the requesting identity is that organisation or acts for it: `Identity.can` on `OwnLine` for a
24
- held line, the identity itself or its `extends_with` for a registration.
25
- - each `Deployment` (`CloudflareDeployment`) sits on one line through `deployment_on_line` and is
26
- visible to whoever holds that line. A deployment is recorded only while its organisation holds
27
- the line. Its label must name the line, and its record must name the line's origin and
28
- environment. [Real-Kernel proof](tests/kernel/README.md).
29
- - an operator deleting an organisation that holds lines deletes its `line_owned_by` Edges in the
30
- same Mutation: the Kernel deletes a Node only together with every incident Edge. Its lines stay
31
- registered with no holder and are refused to every organisation. A transfer is a root
32
- `createEdge` of `line_owned_by` from the line to the new organisation; a line has one holder at
33
- most ([Transfer a line](#transfer-a-line)).
34
- - secret values stay write-only at Cloudflare and never enter Astrale mutations or graph state.
35
- - schedules remain provider state; only the owner may read or replace them.
36
- - logs are remote evidence admitted by the Cloudflare Provider before a Workflow returns them.
37
- - the only admitted Worker service binding is the logical platform router; the provider resolves
38
- it to the selected environment and callers cannot name arbitrary same-account Workers.
39
- - tenant Workers do not receive namespace dispatch authority. Inter-Service requests use public
40
- ingress, including its retirement and private-route checks. Only the platform dispatcher and
41
- candidate-readiness operator retain their respective native dispatch bindings.
42
- - the provider supplies `WORKER_URL` as the Service's canonical HTTPS URL to both candidate and
43
- public Workers. It is reserved provider metadata, not a caller variable or secret. Applications
44
- therefore keep one issuer even when requests arrive over HTTP or through a dispatcher alias.
45
- - `ASTRALE_BUILD_DIGEST` is reserved the same way: a Worker's build digest is bound by its host,
46
- never by its author, so Services refuses it as a caller variable or secret on every deployment
47
- path and in secret management.
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
-
198
- The View, CLI, and agent consumers read the named Service Queries directly; there is intentionally
199
- no list Function wrapping `listServicesQuery`. Deployment, secrets, schedules, logs, and deletion
200
- are Workflows because they coordinate admitted caller intent with external effects. Receiver Methods
201
- are authorized by the ownership Policy before their Workflows begin. `service-hosting`
202
- is the provider-neutral compute Integration contract. Cloudflare is its first Provider.
203
-
204
- `Service` declares seven abstract management contracts. `CloudflareWorker` owns their executable
205
- Methods (`setSecret`, `deleteSecret`, `secrets`, `setSchedule`, `schedules`, `logs`, `migrateIngress`, `delete`) and
206
- applies `OwnService` to each one. Workflow addresses are `CloudflareWorker.*`; abstract `Service.*`
207
- keys are not executable. A future provider Class must supply its own concrete declarations and
208
- implementations. Properties and ownership relationships retain their inherited meaning.
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:
11
+ - **`Service` / `CloudflareService`**: one `(origin, environment)`. Its `name` is the opaque
12
+ 16-character fingerprint the SDK derives from them (`deploymentLine({ addressing: 'opaque' })`);
13
+ it is unique among Services and is the DNS label of `url`, `https://<name>.<routing domain>`.
14
+ `state` is `active`, or `deleting` while its deletion retires its deployments.
15
+ - **`service_owned_by`**: every Service has exactly one owner, an Identity: a user, a group or a
16
+ service account. It is chosen once, when the first deploy creates the Service, and changes only
17
+ by `transfer`.
18
+ - **`Deployment` / `CloudflareDeployment`**: one immutable release of a Service: one build with
19
+ its frozen configuration, served by one Workers for Platforms script at its own URL,
20
+ `https://<label>.<routing domain>`, where `label` is `<name>-<content16>`. It never changes code:
21
+ a new release is a new deployment. Its lifecycle is `deploying` → `active` → `retiring` →
22
+ `retired`, or `failed`; a retired deployment is never served again.
23
+ - **`deployment_of`**: the Service a deployment is a version of. A deployment has no owner of its
24
+ own: it belongs to whoever owns its Service.
25
+ - **`current_deployment`**: the deployment the Service's URL serves and its crons run on. Promoting
26
+ another deployment moves this Edge; a rollback is the promotion of an older one.
27
+
28
+ Secret values stay write-only at Cloudflare and never enter a Mutation or the graph. Schedules
29
+ are provider state. Logs are read from the provider when asked.
30
+
31
+ ## Owner and authority
32
+
33
+ One Policy decides everything: `OwnService`, the subject is the owner of the Service. The Kernel
34
+ evaluates it over the caller and every Identity the caller reaches through `extends_with`, with
35
+ deny-wins. Nothing is selected at check time: a caller in no group reaches its own Services, a
36
+ caller in three groups also reaches the Services of each.
37
+
38
+ - A deploy that creates a Service makes the caller its owner, or the `owner` it names when the
39
+ caller is or acts as that Identity (`ActAs`).
40
+ - A service account deploys a Service when it owns it, or when it is a member of the group that
41
+ owns it (`Group.assignMember` writes the `extends_with` Edge). A member has every right on every
42
+ Service of the group, deletion included.
43
+ - `OwnDeployment` gives a deployment to the owner of its Service.
44
+
45
+ Every Method is `authenticated`: a deployer's identities on the Admin instance hold no `can_use` on
46
+ Services' Functions, which an `authorized` Method requires of every non-Root caller. Each handler
47
+ asks the Kernel `Identity.can` for the caller on its own session before it reads anything: a caller
48
+ who does not own the Service finds none (`SERVICE_NOT_FOUND`, `DEPLOYMENT_NOT_FOUND`), and a deploy
49
+ of another owner's Service is refused `service-owned-elsewhere`. The day the Admin instance grants
50
+ deployers `can_use` on these Methods, they become `authorized` with the Policy and the handler
51
+ checks go.
52
+
53
+ `list` reads Services under the Services Domain credential and keeps those the Kernel answers
54
+ `OwnService` for, in batches: a caller-mode read does not reach the Identity end of
55
+ `service_owned_by` for an owner the caller reaches through `extends_with`
56
+ ([kernel#1138](https://github.com/astrale-os/kernel/issues/1138)).
57
+
58
+ ## Methods
59
+
60
+ Call them on the Admin instance (`astrale call ... --admin`).
61
+
62
+ | Method | On | What it does |
63
+ | --- | --- | --- |
64
+ | `deploy` | `CloudflareService` (static) | Hosts one release (`DeploymentRequestV1`) as a deployment of the Service of its origin and environment, creating the Service on its first deploy, and by default (`promote` absent or `true`) makes the Service serve it. |
65
+ | `hosting` | `CloudflareService` (static) | The routing domain Services and deployments are addressed under; names are always opaque. |
66
+ | `list` | `CloudflareService` (static) | The deployments of the Services the caller owns, narrowed to an origin and environment when given, each with `current`. |
67
+ | `promote` | `CloudflareService` | Points the Service at one of its active deployments, named by its label. |
68
+ | `transfer` | `CloudflareService` | Gives the Service to another owner the caller is or acts as. |
69
+ | `setSecret`, `secrets` | `CloudflareService` | Rewrites one declared secret of the current deployment in place; lists the names it binds, never a value. |
70
+ | `setSchedule`, `schedules` | `CloudflareService` | Replaces or reads the Service's UTC cron set; each cron fires on the current deployment. |
71
+ | `logs` | `CloudflareService` | Recent logs of the current deployment, or of one deployment named by `label`. |
72
+ | `delete` | `CloudflareService` | Stops routing the Service, retires every deployment of it, then deletes it and frees its name. |
73
+ | `retire` | `CloudflareDeployment` | Withdraws one deployment for good. The current deployment is never retired alone. |
74
+
75
+ ## Deploying
76
+
77
+ `astrale-domain deploy <environment>` with the Astrale adapter calls `CloudflareService.deploy`.
78
+ `deploy` is a non-durable Action: its input holds the environment's secrets, and no Execution, step
79
+ result or journal entry keeps them.
80
+
81
+ 1. Before any read it recomputes the label from the Service name, the build digest and the frozen
82
+ configuration, recomputes the build digest from the uploaded bytes, admits the record with the
83
+ SDK, and refuses a service binding, a dispatch namespace, a router other than the logical
84
+ `admin-router`, a reserved binding name, or secrets other than exactly the declared ones.
85
+ 2. It reads the Service of the fingerprint under the Services Domain credential and proves the
86
+ caller's authority on the caller's own session: `OwnService` on an existing Service, `ActAs` on
87
+ the owner it names for a new one.
88
+ 3. A new label is created under a graph lease. The provider uploads it create-only with
89
+ `ASTRALE_BUILD_DIGEST`, `ASTRALE_SETTINGS`, a signing key generated for this deployment alone,
90
+ and the request's secrets; checks that the uploaded version serves its readiness paths; then
91
+ activates it through the routing KV `record:<label>`. The graph records it active with its key
92
+ id, script etag and keyed HMAC-SHA256 digests of its secrets.
93
+ 4. The same release again is the same label: `deploy` answers `reused`. Its secrets are compared
94
+ with the recorded digests: `unchanged`, or rewritten in place on that deployment alone,
95
+ `updated`.
96
+ 5. When it promotes, `current_deployment` moves to the deployment, fenced on what the Service
97
+ served, and the routing KV `current:<name>` names its label. A routing write that failed is
98
+ answered `service-unavailable`; deploying or promoting again converges.
99
+
100
+ `WORKER_URL` and `ASTRALE_BUILD_DIGEST` are bound by the host, never by the author: Services refuses
101
+ them as caller variables or secrets.
102
+
103
+ ## Routing and crons
104
+
105
+ The Services dispatcher serves one routing domain:
106
+
107
+ - `https://<label>.<routing domain>` serves that deployment: `record:<label>` activates it, and
108
+ `retirement:<label>` answers `DEPLOYMENT_RETIRED` (`410`).
109
+ - `https://<name>.<routing domain>` serves the deployment `current:<name>` names, through the same
110
+ path, so a retired deployment is never served through its Service either.
111
+ - Every minute the dispatcher reads the `cron:<name>` keys and posts each due cron to the current
112
+ deployment of that Service, at `https://cron.internal/__scheduled`.
113
+
114
+ ## Secrets
115
+
116
+ Services never keeps a value. A deploy binds the request's secrets to the new deployment only;
117
+ every other deployment keeps its own. `setSecret` rewrites one name the current deployment's frozen
118
+ configuration declares, in place, under the deployment's graph lease: it compares the keyed
119
+ digests, writes only changed values, forgets the digests of the names it writes first and records
120
+ the new ones once every write succeeded. The provider serves a new value a few seconds later. A
121
+ new name, or removing one, is a new release; the signing key never rotates.
122
+
123
+ Never pass a secret value in argv. Write the input to a file only you can read, outside the
124
+ repository:
249
125
 
250
126
  ```json
251
127
  { "name": "API_TOKEN", "value": "<the new value>" }
252
128
  ```
253
129
 
254
130
  ```sh
255
- astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.setSecret" \
131
+ astrale call "@$SERVICE::services.astrale.ai:class.CloudflareService.method.setSecret" \
256
132
  --admin -d @rotate.json --json
257
133
  ```
258
134
 
259
- or pipe it from your secret manager on stdin (`read-new-secret` stands for the command that prints
260
- only the new value):
135
+ Put the new value in the environment's secrets as well: the next deploy binds the values it
136
+ carries.
137
+
138
+ ## Rollback and retirement
261
139
 
262
140
  ```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
141
+ astrale call /:services.astrale.ai:class.CloudflareService:list \
142
+ origin=agencies.example.com environment=production --admin --json
143
+ astrale call "@$SERVICE::services.astrale.ai:class.CloudflareService.method.promote" \
144
+ label=$PREVIOUS_LABEL --admin --json
266
145
  ```
267
146
 
268
- `secrets` lists the names a deployment binds, never a value:
147
+ `retire` withdraws one deployment in two phases: the first call records it `retiring` and writes
148
+ its tombstone, and a call after `retryAfterMs` (once every dispatcher reads the tombstone, about 65
149
+ seconds) deletes its script and key and answers `retired`.
269
150
 
270
151
  ```sh
271
- astrale call "@$ID::services.astrale.ai:class.CloudflareDeployment.method.secrets" --admin --json
152
+ astrale call "@$DEPLOYMENT::services.astrale.ai:class.CloudflareDeployment.method.retire" \
153
+ --admin --json
272
154
  ```
273
155
 
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
-
450
- ## Shared public ingress
451
-
452
- New Services use the environment's proxied wildcard DNS, wildcard Worker route and wildcard
453
- certificate. Services observes the expected dispatcher route and probes a never-registered hostname
454
- before uploading a Worker without an existing Custom Domain. It never creates a per-Service Custom
455
- Domain or certificate. Missing shared ingress is an operator provisioning issue; it is not repaired
456
- by consuming another certificate during a Service deployment.
457
-
458
- After activating routing, deployment waits for public dispatcher visibility with a bounded budget.
459
- This is separate from optional candidate application health checks. An uncertain activation retains
460
- the Service record and can be retried with the same owner-scoped service key.
461
-
462
- Existing exact Custom Domains and their immutable certificate locators are retained on ordinary
463
- updates. Detaching old hostnames is a separate migration, coordinated with Service operations;
464
- upgrading this Runtime does not perform that migration. A Service without an exact domain persists
465
- `tenantDomainState: 'absent'`.
466
- Deletion handles both placements and never removes shared ingress infrastructure.
467
-
468
- Cloudflare callers that must traverse same-zone public Worker routes need
469
- `global_fetch_strictly_public`. Audit existing callers before migrating their exact hostnames;
470
- explicit runtime flags retain their authored meaning. A rollback after creating wildcard Services
471
- must use a Runtime that understands explicit absent placement, not an older certificate-required
472
- implementation.
473
-
474
- ### Migrating existing Services
475
-
476
- The owner calls `CloudflareWorker.migrateIngress` with an `action` (`detach`, `complete`, or
477
- `restore`) and a deterministic public health `probe`: a local GET `path`, expected successful
478
- `status`, and the exact response body's SHA-256 in `bodySha256`. The Workflow acquires the existing
479
- Service operation lease and verifies provider ownership, active dispatch, wildcard TLS and health
480
- before detaching the exact Custom Domain. It does not redeploy the Worker or alter its routing KV,
481
- secrets or schedules.
482
-
483
- After `detach`, observe real traffic for at least 30 minutes before calling `complete`. Until
484
- completion, the graph retains the previous domain and certificate locators with
485
- `tenantDomainDetachedAt`; completion clears them after provider cleanup and another successful
486
- health check. Cloudflare can automatically delete the generated certificate as soon as its Custom
487
- Domain is detached. `restore` recreates the exact domain, waits for active TLS and records the actual
488
- replacement certificate locator. A `pending` result requires a later retry; it is not completion.
489
-
490
- The consumer runtimes are frozen ([`frozen-environments.json`](./frozen-environments.json)): the
491
- original R1 runtime at `https://services.beta.astrale.ai`, which 1Pact instances pin and which
492
- serves that issuer's discovery and keys, `https://services-v2.beta.astrale.ai`, which retains that
493
- issuer and key, and production at `https://services.astrale.ai`. They keep serving the code they run
494
- today and no Environment deploys them. Do not uninstall Services or replace a runtime underneath
495
- installations that still use its Schema. An instance rollback must restore any detached domain
496
- before reinstalling the original endpoint, and must first be qualified with real retained Service
497
- data.
156
+ `delete` marks the Service `deleting`, removes `current:<name>` and `cron:<name>`, and retires every
157
+ deployment. It answers `deleting` with `retryAfterMs` until the last one is retired, then deletes
158
+ the Service with its deployments in one Mutation and answers `deleted`. The name is then free
159
+ again.
498
160
 
499
161
  ## Admin-hosted runtime
500
162
 
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)).
546
-
547
- 1. Read what the Admin instance's Services are bound to, read-only:
548
- `astrale introspect services.astrale.ai -i <admin-instance> --json`. Record `revision` and
549
- `publication.identity.issuer`, and the frozen runtime whose `/.well-known/astrale/domain.json`
550
- declares that revision: it is the rollback URL. Go on only if the issuer is
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.
557
- 2. Create `.env.admin-beta` (mode 600) from `.env.example` with the beta data-plane coordinates the
558
- Admin instance's Services manage today (account, token, dispatch namespace, routing domain
559
- `svc.eu.beta.astrale.ai`, schedule KV, dispatcher, zone, Admin target) and a new
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.
564
- 3. Restore the retained `https://services.beta.astrale.ai` signing identity, the one services-v2 was
565
- deployed with, from managed secret storage to `.astrale/services-beta-identity.json` (mode 600).
566
- Never generate a key for this runtime: the frozen R1 Worker publishes the issuer's keys, so a new
567
- key would never verify. Check that the issuer publishes the restored key:
568
-
569
- ```sh
570
- node --input-type=module -e '
571
- const { readFileSync } = await import("node:fs")
572
- const { kid, x } = JSON.parse(readFileSync(".astrale/services-beta-identity.json", "utf8"))
573
- const issuer = "https://services.beta.astrale.ai"
574
- const { jwks_uri } = await (await fetch(`${issuer}/.well-known/openid-configuration`)).json()
575
- const { keys } = await (await fetch(jwks_uri)).json()
576
- if (!keys.some((key) => key.kid === kid && key.x === x)) throw new Error("key not published")
577
- console.log(`issuer publishes kid ${kid}`)
578
- '
579
- ```
580
-
581
- 4. Deploy once with `pnpm run deploy admin-beta`. It creates only the `services-domain-admin-beta`
582
- Worker and its route; nothing is installed. Do not run `pnpm provider:bootstrap` or a
583
- `provider:route-*` command for this runtime: on the beta data plane they redeploy the shared
584
- dispatcher and DNS routing that serve 1Pact's tenants (see the
585
- [operator README](./providers/cloudflare/operator/README.md)).
586
- 5. Rehearse step 6 first on a disposable instance whose Host runs the Admin Host's Kernel release and
587
- whose Services are bound to the same issuer and revision as in step 1, with at least one Service.
588
- The install must succeed and the Service must stay manageable (list, secrets, logs). Then install
589
- the step 1 rollback URL there again: if the Kernel refuses it, step 6 is one-way, and that must be
590
- recorded before step 6 runs.
591
- 6. Install on the Admin instance only:
592
- `astrale domain install https://services-admin.beta.astrale.ai --allow-identity-override -i <admin-instance>`.
593
- The only consent is the identity override, because the origin `services.astrale.ai` differs from
594
- the serving host; the issuer does not change. On any refusal, stop: never uninstall Services on
595
- the Admin instance to get around it, which would drop the Service records it holds. Never repoint
596
- Admin's catalog entry for `services.astrale.ai` at this runtime: consumer flows, 1Pact's among
597
- them, install Services by origin through that entry.
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:
163
+ Services are hosted for the Admin instance's Kernel alone: its graph is the registry of Service
164
+ names, while the deployments namespace and the routing KV are shared. A Domain Worker serves every
165
+ Kernel that installs it; any other Kernel finds hosting unavailable before anything is read.
692
166
 
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
- ```
167
+ The `admin` Environment sets the plain vars `ASTRALE_ROUTER_WORKER`, `SERVICES_DEPLOYMENT_HOST` (the
168
+ routing domain) and `SERVICES_DEPLOYMENT_KERNEL` (the Admin Kernel's issuer URL). These vars are the
169
+ only declaration of hosting and also bind `SERVICE_DEPLOYMENTS`. Its secrets file, `.env.admin`,
170
+ sets `SERVICES_SECRET_DIGEST_KEY` (at least 32 UTF-8 bytes), `CLOUDFLARE_DISPATCH_NAMESPACE` and
171
+ `CLOUDFLARE_SCHEDULE_KV_ID`; the configuration refuses a file without them or one that sets a
172
+ hosting var.
697
173
 
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
-
734
- ## Plain background Service starter
735
-
736
- The package also ships an isolated scaffolding binary (the root import remains Schema-only):
737
-
738
- ```sh
739
- npm exec --package @astrale-domains/services -- create-astrale-service my-worker
740
- cd my-worker
741
- npm install
742
- # Edit service.json and src/job.js for the existing business Domain.
743
- npm run deploy -- --instance development --as developer
744
- ```
745
-
746
- The generator creates local source only and refuses an existing destination. It does not install
747
- dependencies, create/register accounts, grant permissions, set secrets, enable schedules or deploy.
748
- The generated project owns a plain HTTP/cron entry, an ES256 account session, a job implementation
749
- and build/deploy scripts. No Application, Schema, Publication, Domain signer or frontend is generated.
750
- Deployment calls the public Services API through the installed Astrale CLI and requires an explicit
751
- instance or URL. The project README explains account setup, secret custody, private cron and cleanup.
752
- No tooling dependency is added to the Schema runtime or imported from the package root.
174
+ `pnpm run deploy admin` makes one immutable deployment of this release in the platform dispatch
175
+ namespace. Deploying never installs: install the deployed URL with
176
+ `astrale domain install <url> -i <instance>`. Operator tooling (platform namespace bootstrap,
177
+ dispatcher, routing, audits) is described in
178
+ [`providers/cloudflare/operator/README.md`](providers/cloudflare/operator/README.md).
753
179
 
754
180
  ## Domain development
755
181
 
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.
763
-
764
- To validate without deploying:
182
+ Services is developed against its tests: the fake Cloudflare account and the
183
+ [real-Kernel suite](tests/kernel/README.md).
765
184
 
766
185
  ```sh
767
186
  pnpm typecheck
@@ -771,85 +190,3 @@ pnpm frontend:build
771
190
  pnpm build
772
191
  pnpm package
773
192
  ```
774
-
775
- The deployed module named by `entry` must be present in `modules`. Modules and assets carry
776
- `content: { encoding: 'identity' | 'gzip', decodedSize, contentBase64 }`; large encoded files use
777
- `contentBase64Chunks` instead, with each string bounded to 512 KiB. `decodedSize` is the exact original
778
- byte length. Compression changes transport only: provider uploads and deployment digests use the
779
- reconstructed bytes. Services validates complete gzip streams (including their checksum), exact sizes,
780
- and canonical Base64 before any graph or provider operation. Individual decoded files remain bounded
781
- to 7.5 MiB per module and 12 MiB per asset; all decoded deployment material shares a 32 MiB budget.
782
-
783
- The previous top-level `contentBase64` and `contentBase64Chunks` fields remain accepted and are
784
- explicitly deprecated. Roll out Services and install its new schema on consumer instances before
785
- publishing the compressed SDK adapter. Old SDK clients continue working against the new Services;
786
- new SDK requests are refused by an old schema without provider effects. The adapter never retries a
787
- deployment using a different wire representation after an uncertain outcome.
788
-
789
- Module names are unique and secrets use environment-variable identifiers. Plain and revisioned artifacts are
790
- distinct modes and cannot replace each other at one Service record. Five-field cron expressions are
791
- supported when `CLOUDFLARE_SCHEDULE_KV_ID` is configured.
792
-
793
- Plain deployments that omit `compatibilityFlags` use `nodejs_compat`,
794
- `nodejs_compat_populate_process_env`, and `global_fetch_strictly_public`. Public `fetch()` calls
795
- therefore traverse Cloudflare's public routes, including same-zone Astrale endpoints, instead of
796
- bypassing those routes for the zone's origin. This changes routing, not Kernel authorization.
797
- An explicitly supplied flags array replaces these defaults, including `[]`; low-level callers
798
- own that complete runtime configuration. Existing Workers change only on redeployment.
799
-
800
- Revisioned deployments require explicit runtime configuration and upload it unchanged to both the
801
- candidate and the serving Worker. Those exact flags participate in the deployment identity;
802
- Services must not inject defaults after its computation. The Cloudflare adapter already includes
803
- public-fetch routing in the generated configuration before building the artifact and its digest.
804
-
805
- SDK-authored Domains should deploy through `@astrale-os/adapter-astrale`, which projects the
806
- admitted Cloudflare artifact into the revisioned Services request and verifies the live Publication.
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.
811
-
812
- Revisioned deployments may request `readiness: { paths: ['/health'] }`. Services executes these
813
- bounded local GET paths through its private candidate dispatch binding and requires HTTP 200 before
814
- public promotion. It never follows redirects or reads the response body as Application metadata.
815
- For an existing Service, required checks need every retained secret supplied in the candidate
816
- configuration. Missing values fail before staging or promotion. A caller may explicitly request
817
- `readiness: { paths: ['/health'], required: false }` to preserve updates with Worker-only secrets:
818
- Services then retains those secrets on the serving Worker and returns
819
- `candidateReadiness: 'unavailable-retained-secrets'` without invoking the incomplete candidate.
820
- An executed, successful check returns `candidateReadiness: 'passed'`. Failed checks still refuse
821
- promotion even when `required` is false; this is not a fallback for broken candidates.
822
- Checks share a 30-second budget, shortened by invocation cancellation/deadline. A failed check leaves
823
- the public Worker unchanged and removes the newly owned candidate. Unchecked provider-only requests
824
- retain their existing contract.
825
-
826
- The updated Astrale adapter requests optional `/health` preflight automatically: SDK Workers
827
- initialize their Runtime before serving it. No Project option is needed. The adapter reports
828
- unavailable preflight explicitly and always verifies the public Publication, Bundle, and identity
829
- after deployment. Private preflight alone never proves complete product correctness.
830
- Provision and deploy the updated Services Runtime before using this adapter cohort. Older strict
831
- Services schemas reject the new field; the adapter must not retry without the safety check.
832
-
833
- An uncertain provider promotion is adopted only after observing its exact, non-secret attempt
834
- receipt alongside the artifact tag. Matching code alone is insufficient: Cloudflare's script ETag
835
- can remain unchanged when only secrets change. Services-owned operation leases serialize callers;
836
- operators making direct provider changes must coordinate with those operations. The code ETag is
837
- not a lock for concurrent configuration changes.
838
-
839
- If a Kernel interruption prevents recording a deployment outcome, the Service can remain busy
840
- until its retained operation lease expires (currently 30 minutes from acquisition or renewal).
841
- Retry the same deployment after the expiry reported in the rejection; it reuses the Service and
842
- provider identity. Do not clear the lease or create another Service to bypass an uncertain outcome.
843
- An operation without a recorded expiry needs operator inspection, not a guessed retry deadline.
844
-
845
- A definitively rejected update restores a previously ready Service only when the Provider observes
846
- the same serving script digest and tenant routing coordinates. The restoration is atomic and fenced
847
- by the current operation receipt; it restores the previous name and description and clears the
848
- rejected attempt. The deployment still returns a rejection, but the retained Service remains
849
- manageable. Failed creations, uncertain outcomes, mismatched observations, and unresolved previous
850
- attempts remain failed rather than claiming that the old deployment is ready. This verification adds
851
- no provider request to a successful deployment.
852
-
853
- For the complete live owner-management and non-owner-denial journey, see
854
- [remote acceptance](tests/remote/README.md). Run it against an isolated instance;
855
- ordinary unit tests do not prove callable admission or real provider telemetry.