@starci/hfs 5.2.0 → 5.4.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 (223) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  3. package/cache/hfs-family.d.mts +2 -3
  4. package/cache/hfs-family.mjs +4 -9
  5. package/cache/secret-records.d.mts +7 -0
  6. package/cache/secret-records.mjs +12 -0
  7. package/cache/session.mjs +4 -2
  8. package/codemod/account-doors.d.mts +36 -0
  9. package/codemod/account-doors.mjs +134 -0
  10. package/codemod/app-prettier.d.mts +5 -0
  11. package/codemod/app-prettier.mjs +27 -0
  12. package/codemod/explicit-surface.mjs +1 -25
  13. package/codemod/main.mjs +35 -1
  14. package/codemod/module-definition.d.mts +59 -0
  15. package/codemod/module-definition.mjs +257 -0
  16. package/codemod/quality-binding.d.mts +33 -0
  17. package/codemod/quality-binding.mjs +38 -0
  18. package/codemod/secrets-cache.d.mts +35 -0
  19. package/codemod/secrets-cache.mjs +70 -0
  20. package/codemod/sops-rules.d.mts +34 -0
  21. package/codemod/sops-rules.mjs +69 -0
  22. package/codemod/theme-tokens.mjs +48 -2
  23. package/lint/run.d.mts +6 -0
  24. package/lint/run.mjs +63 -10
  25. package/package.json +1 -1
  26. package/runtime/knowledge/hfs/canon-pins.yaml +43 -10
  27. package/runtime/knowledge/hfs/fe-stack.yaml +3 -3
  28. package/runtime/knowledge/hfs/rules.yaml +13 -9
  29. package/runtime/knowledge/hfs/slots.yaml +105 -12
  30. package/runtime/modules/kernel/failure-codes.yaml +5 -4
  31. package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
  32. package/runtime/scripts/hfs/architecture/config.mjs +25 -1
  33. package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +1 -1
  34. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
  35. package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
  36. package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
  37. package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
  38. package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
  39. package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
  40. package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
  41. package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
  42. package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
  43. package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
  44. package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
  45. package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
  46. package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
  47. package/runtime/scripts/hfs/secret-errors.mjs +8 -0
  48. package/runtime/scripts/hfs/secret-init.d.mts +6 -0
  49. package/runtime/scripts/hfs/secret-init.mjs +23 -0
  50. package/runtime/scripts/hfs/secret-sops.mjs +9 -1
  51. package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
  52. package/runtime/scripts/hfs/secret-standard.mjs +106 -0
  53. package/runtime/scripts/hfs/secret.mjs +5 -1
  54. package/runtime/scripts/hfs/view.d.mts +23 -0
  55. package/runtime/scripts/hfs/view.mjs +68 -0
  56. package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
  57. package/runtime/scripts/lib/declaration-surface.mjs +55 -1
  58. package/runtime/scripts/lib/secret-records.d.mts +13 -0
  59. package/runtime/scripts/lib/secret-records.mjs +83 -0
  60. package/scaffold/add-inbox-lite.mjs +2 -2
  61. package/scaffold/add-queue-lite.mjs +1 -3
  62. package/scaffold/app.d.mts +2 -2
  63. package/scaffold/app.mjs +10 -12
  64. package/scaffold/lite-exports.mjs +1 -15
  65. package/scripts/sync-bundles.d.mts +8 -0
  66. package/stack/images/openclaw-runtime/Dockerfile +18 -0
  67. package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
  68. package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
  69. package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
  70. package/stack/local-up.mjs +3 -0
  71. package/stack/runtime-layers.d.mts +21 -0
  72. package/stack/runtime-layers.mjs +67 -0
  73. package/sync/compile-guard.d.mts +36 -0
  74. package/sync/compile-guard.mjs +189 -0
  75. package/sync/hygiene.mjs +3 -1
  76. package/sync/index.d.mts +11 -1
  77. package/sync/index.mjs +61 -10
  78. package/templates/app/auth-doors.sql.partial +115 -0
  79. package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
  80. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
  81. package/templates/app/package-scripts/core.json +1 -1
  82. package/templates/app/package-scripts/package.json +1 -1
  83. package/templates/app/package-scripts-lite/package.json +1 -1
  84. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +9 -0
  85. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +3 -1
  86. package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +19 -1
  87. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +3 -0
  88. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +5 -0
  89. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +4 -0
  90. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +27 -5
  91. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +33 -0
  92. package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
  93. package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
  94. package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
  95. package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
  96. package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
  97. package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
  98. package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
  99. package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
  100. package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
  101. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
  102. package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +3 -10
  103. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +6 -1
  104. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +3 -10
  105. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +56 -1
  106. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
  107. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
  108. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
  109. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
  110. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
  111. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +30 -30
  112. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +7 -16
  113. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +113 -49
  114. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +42 -53
  115. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
  116. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
  117. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
  118. package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
  119. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
  120. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
  121. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +4 -0
  122. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +3 -10
  123. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +6 -3
  124. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +6 -1
  125. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +17 -0
  126. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +6 -0
  127. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +29 -33
  128. package/templates/be/inbox/module-definition.ts.tpl +2 -9
  129. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
  130. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  131. package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
  132. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
  133. package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
  134. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
  135. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  136. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
  137. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
  138. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
  139. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
  140. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
  141. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
  142. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
  143. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
  144. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
  145. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
  146. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
  147. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
  148. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
  149. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
  150. package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
  151. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
  152. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
  153. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
  154. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
  155. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
  156. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
  157. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
  158. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
  159. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
  162. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
  163. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
  164. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
  165. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
  166. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
  167. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
  168. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
  169. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
  170. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
  171. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
  172. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +3 -1
  173. package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +6 -0
  174. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +3 -0
  175. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +1 -0
  176. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
  177. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +4 -0
  178. package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
  179. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
  180. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
  181. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
  182. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
  183. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
  184. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +2 -9
  185. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +1 -1
  186. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  187. package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
  188. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -0
  189. package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
  190. package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
  191. package/templates/be/table/module-definition.ts.tpl +2 -9
  192. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
  193. package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
  194. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
  195. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
  196. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
  197. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
  198. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
  199. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
  200. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
  201. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
  202. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
  203. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
  204. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
  205. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
  206. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
  207. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
  208. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
  209. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
  210. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
  211. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
  212. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
  213. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
  214. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
  215. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
  216. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
  217. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
  218. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
  219. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
  220. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
  221. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
  222. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +0 -38
  223. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +0 -54
@@ -0,0 +1,13 @@
1
+ /** The cache folder of the secrets family, relative to the cache directory. */
2
+ export declare const SECRETS_FOLDER = "check/secrets";
3
+ /** A file of the secrets folder in a tracked tree: `.starcistacks/cache/check/secrets/<area>.jsonl` (an area is a folder path, so the file may sit in subfolders), at the app root or below one. */
4
+ export declare const SECRETS_CACHE_FILE: RegExp;
5
+ /** The finding codes of the secrets family: plaintext secrets, sealed custody, untracked secret files. */
6
+ export declare const SECRET_CODES: ReadonlySet<string>;
7
+ /** The stored form of a finding message: the digest of the message. A message already in that form is kept. */
8
+ export declare function messageDigest(message: string): string;
9
+ /**
10
+ * The faults of the text of a file of the secrets folder: every line is a record that holds codes, paths, positions and digests, and nothing
11
+ * else. An empty list means the file carries no secret material by construction.
12
+ */
13
+ export declare function secretRecordFaults(text: string): string[];
@@ -0,0 +1,83 @@
1
+ // secret-records.mjs - the contract of the secrets family's records in the committed check cache (contexts/project/check-cache.md, "The secrets
2
+ // records"). The cache is committed, so what the secrets family writes may never be secret material: a record holds codes, paths, positions
3
+ // and hashes. The finding's rule is one of the family's codes, its message is the sha256 prefix of the message the run printed (the identity
4
+ // the new / known / fixed comparison needs), and the record carries no `data`. The cache writer (hfs cache/secret-records.mjs) projects its
5
+ // records to this form; the secret judgement of R06 (scripts/hfs/rules/secrets.mjs, shared by `starci app check` and the commit hook) judges a
6
+ // staged or tracked file of the folder by `secretRecordFaults`, so the file is accepted by what it holds and never by where it sits.
7
+ import { createHash } from 'node:crypto';
8
+ /** The cache folder of the secrets family, relative to the cache directory. */
9
+ export const SECRETS_FOLDER = 'check/secrets';
10
+ /** A file of the secrets folder in a tracked tree: `.starcistacks/cache/check/secrets/<area>.jsonl` (an area is a folder path, so the file may sit in subfolders), at the app root or below one. */
11
+ export const SECRETS_CACHE_FILE = /(?:^|\/)\.starcistacks\/cache\/check\/secrets\/.+\.jsonl$/;
12
+ /** The finding codes of the secrets family: plaintext secrets, sealed custody, untracked secret files. */
13
+ export const SECRET_CODES = new Set([
14
+ 'HFS_PLAINTEXT_SECRET', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_LITE_SECRET_CUSTODY', 'HFS_IDENTITY_CUSTODY',
15
+ 'HFS_AGE_KEY_CUSTODY', 'RT_SECRET_TRACKED', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'SEALED_CUSTODY_LOCATION',
16
+ 'APP_SECRET_IN_CI', 'CI_SECRET_SEALED', 'BE_LOG_SECRET', 'BE_SECRET_DEFAULT', 'HFS_DOCKER_SECRETS',
17
+ ]);
18
+ /** The form a stored message takes: `sha256:` and the first 16 hex digits of the sha256 of the message. */
19
+ const DIGEST = /^sha256:[0-9a-f]{16}$/;
20
+ /** A path a record may name: a relative posix path without control characters, backslashes or colons, never absolute and never leaving the app. */
21
+ const SAFE_PATH = /^(?!\/)(?!(?:.*\/)?\.\.(?:\/|$))[^\u0000-\u001f\\:]*$/u;
22
+ /** The stored form of a finding message: the digest of the message. A message already in that form is kept. */
23
+ export function messageDigest(message) {
24
+ return DIGEST.test(message) ? message : `sha256:${createHash('sha256').update(message).digest('hex').slice(0, 16)}`;
25
+ }
26
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
27
+ const isPath = (value) => typeof value === 'string' && SAFE_PATH.test(value);
28
+ /** The faults of one parsed record of the secrets folder; an empty list is a record that carries no secret material. */
29
+ function recordFaults(value, at) {
30
+ if (!isObject(value))
31
+ return [`${at} is not a record`];
32
+ const faults = [];
33
+ const extra = Object.keys(value).filter((key) => !['path', 'step', 'hash', 'findings'].includes(key));
34
+ if (extra.length > 0)
35
+ faults.push(`${at} carries ${extra.join(', ')}; a secrets record holds path, step, hash and findings only`);
36
+ if (!isPath(value['path']))
37
+ faults.push(`${at} path is not a relative posix path`);
38
+ if (value['step'] !== 'secrets')
39
+ faults.push(`${at} step is not "secrets"`);
40
+ if (typeof value['hash'] !== 'string' || !/^(?:[0-9a-f]{64}|absent)$/.test(value['hash']))
41
+ faults.push(`${at} hash is not a sha256`);
42
+ const findings = value['findings'];
43
+ if (!Array.isArray(findings))
44
+ return [...faults, `${at} findings is not a list`];
45
+ for (const finding of findings) {
46
+ if (!isObject(finding)) {
47
+ faults.push(`${at} holds a finding that is not an object`);
48
+ continue;
49
+ }
50
+ const keys = Object.keys(finding).sort().join(',');
51
+ if (keys !== 'column,line,message,path,rule')
52
+ faults.push(`${at} holds a finding with the keys ${keys}; a finding holds path, line, column, rule and message only`);
53
+ if (typeof finding['rule'] !== 'string' || !SECRET_CODES.has(finding['rule']))
54
+ faults.push(`${at} holds a finding whose rule is not a code of the secrets family`);
55
+ if (typeof finding['message'] !== 'string' || !DIGEST.test(finding['message']))
56
+ faults.push(`${at} holds a finding whose message is not a digest (sha256:<16 hex>)`);
57
+ if (!isPath(finding['path']))
58
+ faults.push(`${at} holds a finding whose path is not a relative posix path`);
59
+ if (!Number.isInteger(finding['line']) || !Number.isInteger(finding['column']))
60
+ faults.push(`${at} holds a finding whose position is not a pair of integers`);
61
+ }
62
+ return faults;
63
+ }
64
+ /**
65
+ * The faults of the text of a file of the secrets folder: every line is a record that holds codes, paths, positions and digests, and nothing
66
+ * else. An empty list means the file carries no secret material by construction.
67
+ */
68
+ export function secretRecordFaults(text) {
69
+ const faults = [];
70
+ text.split('\n').forEach((raw, index) => {
71
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
72
+ if (line.trim() === '')
73
+ return;
74
+ const at = `line ${index + 1}`;
75
+ try {
76
+ faults.push(...recordFaults(JSON.parse(line), at));
77
+ }
78
+ catch {
79
+ faults.push(`${at} is not JSON`);
80
+ }
81
+ });
82
+ return faults;
83
+ }
@@ -218,8 +218,8 @@ export function addLiteWebhookInbox({ root, repo, noun, provider, values, now =
218
218
  const hasHandoffStorage = migrations.some((file) => /create\s+function\s+public\.event_bus_send\b/i.test(fs.readFileSync(path.join(migrationsDir, file), "utf8")));
219
219
  if (!hasHandoffStorage)
220
220
  planned[0].text = `${template("platform/migration.sql.tpl", renderValues)}\n${template("platform/client-operation-outcomes.sql.tpl", renderValues)}\n${planned[0].text}`; // the migration is the first planned file
221
- // The sql tag and the database-function call (with its spec) the inbox and the event bus use; the lite starter leaves them out.
222
- for (const file of ["database.sql.ts", "database.client.ts", "database.client.spec.ts"]) {
221
+ // The sql tag the inbox and the event bus use; the lite starter leaves it out.
222
+ for (const file of ["database.sql.ts"]) {
223
223
  const relative = `be/src/modules/platform/database/${file}`;
224
224
  if (fs.existsSync(path.join(root, ...relative.split("/"))))
225
225
  continue;
@@ -79,13 +79,11 @@ const template = (relative) => read(path.join(TEMPLATES_DIR, ...relative.split('
79
79
  const DATABASE_ERROR = 'be/src/modules/platform/database/errors/database.error.ts';
80
80
  /** Skeleton files the lite starter leaves out and the queue capabilities use, each from its skeleton template. */
81
81
  const SKELETON_FILES = Object.freeze([
82
- ['be/src/modules/platform/database/database.client.ts', 'skeleton-lite/src/modules/platform/database/database.client.ts'],
83
- ['be/src/modules/platform/database/database.client.spec.ts', 'skeleton-lite/src/modules/platform/database/database.client.spec.ts'],
84
82
  ['be/src/modules/platform/database/database.sql.ts', 'skeleton-lite/src/modules/platform/database/database.sql.ts'],
85
83
  ['be/src/modules/platform/primitives/sequence.policy.ts', 'skeleton/src/modules/platform/primitives/sequence.policy.ts'],
86
84
  ['be/src/modules/platform/primitives/sequence.policy.spec.ts', 'skeleton/src/modules/platform/primitives/sequence.policy.spec.ts'],
87
85
  ]);
88
- /** The database-function call and the sql tag of the outbox and claim statements (with its identifier error) and the in-order relay the queue adapters walk their rows with. */
86
+ /** The sql tag of the outbox and claim statements (with its identifier error) and the in-order relay the queue adapters walk their rows with. */
89
87
  function skeletonFiles(repoRoot) {
90
88
  const created = SKELETON_FILES.filter(([relative]) => !fs.existsSync(path.join(repoRoot, ...relative.split('/'))))
91
89
  .map(([relative, source]) => ({ relative, text: render(template(`be/${source}`), {}) }));
@@ -160,8 +160,8 @@ export declare const PARTIAL_VARIABLES: any;
160
160
  * The be files a generator writes when it first needs them, never the starter, so a fresh app carries no file and no export that
161
161
  * nothing reads (R25). In both editions: the entity bases of platform/database (abstract.entity.ts, BE-PERSISTENCE-3), written by
162
162
  * the first table whose entity extends them, since no starter table has both timestamps. In lite also: the cli's runners, the in-order
163
- * relay, and the raw SQL helpers of platform/database (the sql tag, and the database-function call statement with its spec), which only
164
- * the table, queue, jobs and webhook capabilities call; a fresh app reads and writes through entities alone (R334).
163
+ * relay, and the sql tag of platform/database, which only the table, queue, jobs and webhook capabilities call; a fresh app reads and
164
+ * writes through entities and, for the account doors of its sign-in, the database-function call of database.client.ts (R334).
165
165
  */
166
166
  export declare const ENTITY_BASE_FILE = "src/modules/platform/database/abstract.entity.ts";
167
167
  /**
package/scaffold/app.mjs CHANGED
@@ -139,7 +139,7 @@ const LITE_STARTER_SIDES = Object.freeze({
139
139
  const STARTER_DEPENDENCIES = Object.freeze({
140
140
  dependencies: {
141
141
  '@apollo/server': null, '@nestjs/apollo': null, '@nestjs/common': null, '@nestjs/core': null, '@nestjs/cqrs': null, '@nestjs/graphql': null,
142
- '@nestjs/platform-express': null, '@nestjs/typeorm': null, 'nest-commander': null, pg: null, graphql: null,
142
+ '@nestjs/platform-express': null, '@nestjs/typeorm': null, '@starci/nest-kit': null, 'nest-commander': null, pg: null, graphql: null,
143
143
  'class-transformer': null, 'class-validator': null, 'reflect-metadata': null, rxjs: null, tslib: null, typeorm: null, undici: null, zod: null,
144
144
  },
145
145
  devDependencies: {
@@ -158,7 +158,7 @@ const STARTER_DEPENDENCIES = Object.freeze({
158
158
  const LITE_STARTER_DEPENDENCIES = Object.freeze({
159
159
  dependencies: {
160
160
  '@apollo/server': null, '@nestjs/apollo': null, '@nestjs/common': null, '@nestjs/core': null, '@nestjs/cqrs': null, '@nestjs/graphql': null,
161
- '@nestjs/platform-express': null, '@nestjs/typeorm': null, '@starci/auth': null, '@starci/object-storage': null,
161
+ '@nestjs/platform-express': null, '@nestjs/typeorm': null, '@starci/auth': null, '@starci/mail': null, '@starci/nest-kit': null, '@starci/object-storage': null,
162
162
  'class-transformer': null, 'class-validator': null, graphql: null, pg: null,
163
163
  'reflect-metadata': null, rxjs: null, tslib: null, typeorm: null, undici: null, zod: null,
164
164
  },
@@ -182,22 +182,22 @@ const FE_APP_DEPENDENCIES = Object.freeze({
182
182
  '@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@xyflow/react': null, 'react-aria-components': null, next: null, 'next-intl': null, react: null, 'react-dom': null,
183
183
  });
184
184
  /**
185
- * The lite fe workspace: the same base without the shared packages, plus server-only for its own next-intl request config. It
186
- * holds no identity SDK: modules/auth talks only to the app's own back-end auth doors.
185
+ * The lite fe workspace: the same base without the shared packages (its next-intl stack is @starci/next-i18n's, a core package of the
186
+ * fe stack). It holds no identity SDK: modules/auth talks only to the app's own back-end auth doors.
187
187
  */
188
188
  const LITE_FE_APP_DEPENDENCIES = Object.freeze({
189
189
  '@heroicons/react': null, '@heroui/react': null, '@heroui/styles': null, '@starci/auth': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null, 'react-aria-components': null,
190
- next: null, 'next-intl': null, react: null, 'react-dom': null, 'server-only': null,
190
+ next: null, 'next-intl': null, react: null, 'react-dom': null,
191
191
  });
192
192
  /**
193
193
  * The shared fe workspace packages of the starter, fe/packages/<project>-<name> named @<project>/<name>: each one's exported
194
194
  * subpaths (built to dist) and the runtime dependencies its own source imports. ui holds the drawings both apps mount; i18n the
195
- * next-intl stack (the entry, a server module: createAppI18n; ./proxy, the locale negotiation the apps' proxy.ts re-exports;
196
- * ./routing, client-safe: the default locale a client boundary reads).
195
+ * product's description of its languages over @starci/next-i18n (the entry, a server module: createAppI18n; ./proxy, the locale
196
+ * negotiation the apps' proxy.ts re-exports; ./routing, client-safe: the description and the default locale a client boundary reads).
197
197
  */
198
198
  const FE_PACKAGES = Object.freeze({
199
199
  ui: { exports: ['.'], dependencies: { '@heroui/react': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null, 'react-aria-components': null, 'next-intl': null, react: null, 'react-dom': null } },
200
- i18n: { exports: ['.', './proxy', './routing'], dependencies: { next: null, 'next-intl': null, 'server-only': null } },
200
+ i18n: { exports: ['.', './proxy', './routing'], dependencies: { '@starci/next-i18n': null, next: null, 'server-only': null } },
201
201
  });
202
202
  /** The scripts of every fe package workspace: tsc builds it to dist and type-checks it. */
203
203
  const PACKAGE_WORKSPACE_SCRIPTS = Object.freeze({ build: 'tsc -p tsconfig.build.json', typecheck: 'tsc --noEmit -p tsconfig.json' });
@@ -333,15 +333,13 @@ const LITE_APP_REUSED = new Set(['.editorconfig', '.gitattributes', '.nvmrc']);
333
333
  * The be files a generator writes when it first needs them, never the starter, so a fresh app carries no file and no export that
334
334
  * nothing reads (R25). In both editions: the entity bases of platform/database (abstract.entity.ts, BE-PERSISTENCE-3), written by
335
335
  * the first table whose entity extends them, since no starter table has both timestamps. In lite also: the cli's runners, the in-order
336
- * relay, and the raw SQL helpers of platform/database (the sql tag, and the database-function call statement with its spec), which only
337
- * the table, queue, jobs and webhook capabilities call; a fresh app reads and writes through entities alone (R334).
336
+ * relay, and the sql tag of platform/database, which only the table, queue, jobs and webhook capabilities call; a fresh app reads and
337
+ * writes through entities and, for the account doors of its sign-in, the database-function call of database.client.ts (R334).
338
338
  */
339
339
  export const ENTITY_BASE_FILE = 'src/modules/platform/database/abstract.entity.ts';
340
340
  const DEFERRED_GENERATOR_FILES = new Set([ENTITY_BASE_FILE]);
341
341
  const LITE_DEFERRED_GENERATOR_FILES = new Set([
342
342
  ...DEFERRED_GENERATOR_FILES,
343
- 'src/modules/platform/database/database.client.ts',
344
- 'src/modules/platform/database/database.client.spec.ts',
345
343
  'src/modules/platform/database/database.sql.ts',
346
344
  'src/modules/platform/database/migration-runner.service.ts',
347
345
  'src/modules/platform/database/seed-runner.service.ts',
@@ -59,20 +59,13 @@ const REGISTRATIONS = Object.freeze({
59
59
  names: ["sql"],
60
60
  beforeSource: "./errors/database.error",
61
61
  },
62
- {
63
- // The outbox write and the relay call the queue functions of their migration (R334).
64
- file: "be/src/modules/platform/database/index.ts",
65
- source: "./database.client",
66
- names: ["callDbFunction"],
67
- beforeSource: "./primary.decorators",
68
- },
69
62
  ],
70
63
  // The jobs capability (add job, the fenced-job pattern): its tick takes a transaction-scoped lock before it claims the minute (R335).
71
64
  jobs: [
72
65
  {
73
66
  file: "be/src/modules/platform/database/index.ts",
74
67
  source: "./database.client",
75
- names: ["advisoryXactLock", "callDbFunction"],
68
+ names: ["advisoryXactLock"],
76
69
  beforeSource: "./primary.decorators",
77
70
  },
78
71
  ],
@@ -83,13 +76,6 @@ const REGISTRATIONS = Object.freeze({
83
76
  names: ["BATCH_ROWS", "sql"],
84
77
  beforeSource: "./errors/database.error",
85
78
  },
86
- {
87
- // The inbox claim and the lite event bus call the functions of their migration (R334).
88
- file: "be/src/modules/platform/database/index.ts",
89
- source: "./database.client",
90
- names: ["callDbFunction"],
91
- beforeSource: "./primary.decorators",
92
- },
93
79
  {
94
80
  // PRIMARY_ENTITY_MANAGER: the token the inbox service's unit spec provides its EntityManager double under.
95
81
  file: "be/src/modules/platform/database/index.ts",
@@ -29,3 +29,11 @@ export declare function syncBundles(options: {
29
29
  readonly check: boolean;
30
30
  readonly packages?: readonly string[];
31
31
  }): SyncResult;
32
+ /** The harness runtime layers whose recipe hfs ships for `starci app stack up` (stack/runtime-layers.mts): the folder name under images/ of the harness repository. */
33
+ export declare const layerRecipes: readonly ["openclaw-runtime"];
34
+ /** Syncs (or judges) the shipped copy stack/images/<layer>/ of each recipe against the owner, `images/<layer>/` of the harness repository. */
35
+ export declare function syncLayerRecipes(options: {
36
+ readonly imagesDir: string;
37
+ readonly shippedDir: string;
38
+ readonly check: boolean;
39
+ }): SyncResult;
@@ -0,0 +1,18 @@
1
+ # StarCi OpenClaw runtime layer: the harness-pinned source-built fork (images/openclaw, sources.json) plus the generic gateway wrapper.
2
+ # An app image does `FROM ghcr.io/starci183/openclaw-runtime@sha256:...` and only COPYs its own plugins into /opt/starci/plugins/<name>/;
3
+ # it carries no wrapper code (images/README.md "The OpenClaw runtime layer"). The base below is the recorded `published.reference` of the
4
+ # `openclaw` entry of sources.json; a new fork build raises it here in the same change.
5
+ FROM ghcr.io/starci183/openclaw:2026.10.10-beta.5-starci.1@sha256:0770c1da11ec4a46607ec16957ac3cff68d05db4a287f59f0e1d0e76de980ff8
6
+
7
+ USER root
8
+ # The wrapper is root-owned and not writable by the runtime user; the plugin directory is root-owned and world-readable, so a plugin an
9
+ # app copies in cannot be rewritten by the gateway it runs inside.
10
+ COPY --chmod=0555 entrypoint.sh /opt/starci/entrypoint.sh
11
+ COPY --chmod=0444 openclaw-init.mjs openclaw-relay.mjs /opt/starci/
12
+ RUN mkdir -p /opt/starci/plugins \
13
+ && chown -R root:root /opt/starci \
14
+ && chmod 0755 /opt/starci /opt/starci/plugins
15
+
16
+ USER node
17
+ ENV STARCI_PLUGINS_DIR=/opt/starci/plugins
18
+ ENTRYPOINT ["tini", "-s", "--", "sh", "/opt/starci/entrypoint.sh"]
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+ # Entrypoint of the StarCi OpenClaw runtime layer (/opt/starci/entrypoint.sh).
3
+ #
4
+ # 1. The OpenClaw image has no `<NAME>_FILE` support, so every secret named in STARCI_SECRETS (space separated; OPENCLAW_GATEWAY_TOKEN is always
5
+ # read) is exported from /run/secrets/<NAME> into THIS process tree only: no env file on the host, no value in the service specification.
6
+ # 2. openclaw-init.mjs merges the gateway settings into openclaw.json (idempotent; other keys, agents and plugins are kept) and enables the plugins
7
+ # found under STARCI_PLUGINS_DIR.
8
+ # 3. openclaw-relay.mjs listens on the overlay address and forwards to the loopback-only gateway: the gateway honours the scopes a client
9
+ # declares only for a loopback peer using the shared secret, so clients connect to the relay (OPENCLAW_RELAY_PORT, default 18790).
10
+ set -eu
11
+
12
+ for name in OPENCLAW_GATEWAY_TOKEN ${STARCI_SECRETS:-}; do
13
+ file="/run/secrets/$name"
14
+ if [ -r "$file" ]; then
15
+ value="$(cat "$file")"
16
+ export "$name=$value"
17
+ fi
18
+ done
19
+ : "${OPENCLAW_GATEWAY_TOKEN:?the secret OPENCLAW_GATEWAY_TOKEN is not granted to this service}"
20
+
21
+ OPENCLAW_GATEWAY_TOKEN_FILE=/run/secrets/OPENCLAW_GATEWAY_TOKEN node /opt/starci/openclaw-init.mjs
22
+ node /opt/starci/openclaw-relay.mjs &
23
+ exec node openclaw.mjs gateway
@@ -0,0 +1,101 @@
1
+ // Writes the gateway settings the StarCi runtime layer relies on into openclaw.json before the gateway starts (idempotent merge: other keys,
2
+ // agents and plugins that OpenClaw or an operator added are kept). Loopback bind + token auth: the gateway is reached only through the relay.
3
+ //
4
+ // The contract an app image relies on (images/README.md "The OpenClaw runtime layer"):
5
+ // STARCI_PLUGINS_DIR the plugin directory (default /opt/starci/plugins): every subfolder holding an openclaw.plugin.json is added to
6
+ // plugins.load.paths and enabled as plugins.entries.<folder name>
7
+ // AI_BASE_URL + STARCI_AI_KEY one OpenAI-compatible custom provider (name OPENCLAW_PROVIDER, default "starci-ai"; chat model AI_CHAT_MODEL,
8
+ // default "work"); the key stays a reference openclaw resolves from the gateway process environment
9
+ // OPENCLAW_MODEL the primary model of the agents; OPENCLAW_MODEL_REASONING=off switches the model's reasoning off
10
+ import fs from "node:fs"
11
+ import path from "node:path"
12
+
13
+ const file = "/home/node/.openclaw/openclaw.json"
14
+ let config = {}
15
+ try {
16
+ config = JSON.parse(fs.readFileSync(file, "utf8"))
17
+ } catch {
18
+ config = {}
19
+ }
20
+ // The app writes each agent workspace itself: no first-run ritual.
21
+ config.agents = config.agents || {}
22
+ config.agents.defaults = { ...(config.agents.defaults || {}), skipBootstrap: true }
23
+ // The app owns every trigger and schedule: OpenClaw runs one turn when asked and nothing in the background. `cron.enabled: false` stops its cron
24
+ // scheduler; a `defaults.heartbeat` block makes every agent resolve its heartbeat from it, and `every: "0m"` gives each no interval.
25
+ config.cron = { ...(config.cron || {}), enabled: false }
26
+ config.agents.defaults.heartbeat = { ...(config.agents.defaults.heartbeat || {}), every: "0m" }
27
+ // Plugins: every folder of the plugin directory that holds an openclaw.plugin.json is loaded and enabled; other paths and entries are kept.
28
+ const pluginsDir = process.env.STARCI_PLUGINS_DIR || "/opt/starci/plugins"
29
+ const found = fs.existsSync(pluginsDir)
30
+ ? fs.readdirSync(pluginsDir, { withFileTypes: true })
31
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(pluginsDir, entry.name, "openclaw.plugin.json")))
32
+ .map((entry) => entry.name)
33
+ .sort()
34
+ : []
35
+ const plugins = config.plugins || {}
36
+ const load = plugins.load || {}
37
+ const paths = Array.isArray(load.paths) ? load.paths : []
38
+ const entries = { ...(plugins.entries || {}) }
39
+ for (const name of found) entries[name] = { ...(entries[name] || {}), enabled: true }
40
+ config.plugins = {
41
+ ...plugins,
42
+ load: { ...load, paths: [...paths, ...found.map((name) => path.join(pluginsDir, name)).filter((folder) => !paths.includes(folder))] },
43
+ entries,
44
+ }
45
+ config.session = {
46
+ ...(config.session || {}),
47
+ maintenance: {
48
+ ...((config.session || {}).maintenance || {}),
49
+ mode: "enforce",
50
+ pruneAfter: "7d",
51
+ maxEntries: 400,
52
+ maxDiskBytes: "400mb",
53
+ },
54
+ }
55
+ config.gateway = {
56
+ ...(config.gateway || {}),
57
+ mode: "local",
58
+ bind: "loopback",
59
+ port: 18789,
60
+ auth: {
61
+ ...((config.gateway || {}).auth || {}),
62
+ mode: "token",
63
+ token: fs.readFileSync(process.env.OPENCLAW_GATEWAY_TOKEN_FILE, "utf8"),
64
+ },
65
+ }
66
+ // Language models come only from the AI gateway the app hosts: one OpenAI-compatible custom provider with the virtual key it shares with the app.
67
+ if (process.env.AI_BASE_URL && process.env.STARCI_AI_KEY) {
68
+ const chat = process.env.AI_CHAT_MODEL || "work"
69
+ const provider = process.env.OPENCLAW_PROVIDER || "starci-ai"
70
+ config.models = {
71
+ ...(config.models || {}),
72
+ mode: "merge",
73
+ providers: {
74
+ ...((config.models || {}).providers || {}),
75
+ [provider]: {
76
+ baseUrl: process.env.AI_BASE_URL,
77
+ apiKey: "${STARCI_AI_KEY}",
78
+ api: "openai-completions",
79
+ models: [{ id: chat, name: chat, input: ["text", "image"] }],
80
+ },
81
+ },
82
+ }
83
+ }
84
+ const model = process.env.OPENCLAW_MODEL
85
+ if (model) {
86
+ config.agents.defaults.model = { ...(config.agents.defaults.model || {}), primary: model }
87
+ if (process.env.OPENCLAW_MODEL_REASONING === "off") {
88
+ const models = (config.agents.defaults.models = config.agents.defaults.models || {})
89
+ const previous = models[model] || {}
90
+ models[model] = {
91
+ ...previous,
92
+ params: {
93
+ ...(previous.params || {}),
94
+ extra_body: { ...((previous.params || {}).extra_body || {}), reasoning: { enabled: false } },
95
+ },
96
+ }
97
+ }
98
+ }
99
+ fs.mkdirSync("/home/node/.openclaw", { recursive: true })
100
+ fs.writeFileSync(file, JSON.stringify(config, null, 2), { mode: 0o600 })
101
+ console.log("openclaw.json ready (gateway local/loopback/token, " + found.length + " plugin" + (found.length === 1 ? "" : "s") + (model ? ", model " + model : "") + ")")
@@ -0,0 +1,25 @@
1
+ // A byte-for-byte TCP relay inside the OpenClaw task: it listens on the overlay address and forwards every connection to the loopback-only
2
+ // gateway (127.0.0.1:18789), so the gateway sees a loopback peer and honours the scopes a client declares. It adds no header and reads no frame.
3
+ import net from "node:net"
4
+
5
+ const listenPort = Number(process.env.OPENCLAW_RELAY_PORT || 18790)
6
+ const gatewayPort = 18789
7
+
8
+ const server = net.createServer((client) => {
9
+ const gateway = net.connect({ host: "127.0.0.1", port: gatewayPort })
10
+ const close = () => {
11
+ client.destroy()
12
+ gateway.destroy()
13
+ }
14
+ client.on("error", close)
15
+ gateway.on("error", close)
16
+ client.on("close", close)
17
+ gateway.on("close", close)
18
+ client.pipe(gateway)
19
+ gateway.pipe(client)
20
+ })
21
+ server.on("error", (error) => {
22
+ console.error("openclaw relay failed: " + error.message)
23
+ process.exit(1)
24
+ })
25
+ server.listen(listenPort, "0.0.0.0", () => console.log("openclaw relay " + listenPort + " -> 127.0.0.1:" + gatewayPort))
@@ -11,6 +11,7 @@ import { missingKeys, missingStep, openCustody } from './local-custody.mjs';
11
11
  import { hostRefusal, preflight, runSetup } from './local-prepare.mjs';
12
12
  import { DEFAULT_TIMEOUT_SECONDS, makeDataDirectories, commandDigest, compose, composeValues, databaseUrl, isAlive, probe, runCommand, runEnvironment, serviceReady, startProcess, stateOf, waitFor } from './local-run.mjs';
13
13
  import { readRecord, writeRecord } from './local-state.mjs';
14
+ import { ensureRuntimeLayers, layerPlan } from './runtime-layers.mjs';
14
15
  const SAFE_PASSWORD = /^[A-Za-z0-9_-]{8,}$/u;
15
16
  /** The digest of the migration folder (names and contents): what a migrate receipt records. */
16
17
  export function migrationsDigest(repoRoot) {
@@ -122,6 +123,7 @@ function plan(local, missing) {
122
123
  return [
123
124
  `plan custody: ${missing.length ? `missing ${missing.join(', ')}: ${missingStep(local, missing)}` : 'open the sealed dev values into host-local files'}`,
124
125
  ...local.setup.map((run) => `plan setup ${run.name}: ${run.argv.join(' ')}`),
126
+ ...layerPlan(local),
125
127
  `plan services: ${local.services.map((service) => `${service.name} (wait ${service.wait})`).join(', ')}`,
126
128
  ...(local.database ? [`plan database: own role ${local.database.role}, database ${local.database.name}, then starci app db migrate`] : []),
127
129
  ...local.seed.map((run) => `plan seed ${run.name}: ${run.argv.join(' ')}`),
@@ -142,6 +144,7 @@ export async function upLocal(ctx, local, { execute = false, timeoutSeconds = DE
142
144
  lines.push(`custody: ${custody.values.size} key file(s) in the host-local directory`);
143
145
  lines.push(...await runSetup(ctx, local, custody));
144
146
  makeDataDirectories(ctx, local);
147
+ lines.push(...await ensureRuntimeLayers(ctx, local));
145
148
  const values = composeValues(local, custody);
146
149
  const started = await compose(ctx, local, ['up', '-d', ...local.services.map((service) => service.name)], { values, heavy: true, label: 'compose up' });
147
150
  if (started.status !== 0)
@@ -0,0 +1,21 @@
1
+ import type { StackContext } from './context.mjs';
2
+ import type { LocalConfig } from './local-config.mjs';
3
+ /** The registry path of the harness-owned layers: `<prefix><layer>`. */
4
+ export declare const LAYER_PREFIX = "ghcr.io/starci183/";
5
+ /** The layers whose recipe this package ships. */
6
+ export declare const SHIPPED_LAYERS: readonly string[];
7
+ /** The folder holding the recipe of a layer in this package. */
8
+ export declare const recipeDirectory: (layer: string) => string;
9
+ /** One layer an app image builds on: the layer, the reference as the Dockerfile writes it and the tag a local build gives it. */
10
+ export interface LayerUse {
11
+ layer: string;
12
+ reference: string;
13
+ tag: string;
14
+ dockerfile: string;
15
+ }
16
+ /** The layers `.starcistacks/images/*` of the app builds on (each distinct reference once), from the `FROM` lines of its Dockerfiles. */
17
+ export declare function layersUsed(repoRoot: string): LayerUse[];
18
+ /** The lines of the plan a dry run prints for the layers the app builds on. */
19
+ export declare const layerPlan: (local: LocalConfig) => string[];
20
+ /** Builds every layer the app names that the host neither holds nor can pull; returns one line per layer. */
21
+ export declare function ensureRuntimeLayers(ctx: StackContext, local: LocalConfig): Promise<string[]>;
@@ -0,0 +1,67 @@
1
+ // runtime-layers.mjs - the harness runtime layers a dev stack builds on (images/README.md "The OpenClaw runtime layer"; R374 STACKS_IMAGE_BASE_NOT_FORK).
2
+ // An app image folder `.starcistacks/images/<name>/Dockerfile` is `FROM ghcr.io/starci183/<layer>:<tag>[@sha256:<digest>]`. While the harness has not
3
+ // published that layer (or the host cannot pull it), `starci app stack up` builds it from the recipe this package ships (stack/images/<layer>/,
4
+ // byte-equal to the harness repository's images/<layer>/, which `npm run bundles` copies and a spec compares) and tags it with the reference the
5
+ // app names minus its digest, so the compose build of the app image finds it in the local store. The build is a governed heavy command
6
+ // (hard rule 8): an agent never runs docker by hand. A reference whose image the host already holds, or can pull, is left alone.
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { failure } from './errors.mjs';
11
+ /** The registry path of the harness-owned layers: `<prefix><layer>`. */
12
+ export const LAYER_PREFIX = 'ghcr.io/starci183/';
13
+ /** The layers whose recipe this package ships. */
14
+ export const SHIPPED_LAYERS = Object.freeze(['openclaw-runtime']);
15
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
16
+ /** The folder holding the recipe of a layer in this package. */
17
+ export const recipeDirectory = (layer) => path.join(HERE, 'images', layer);
18
+ /** The layers `.starcistacks/images/*` of the app builds on (each distinct reference once), from the `FROM` lines of its Dockerfiles. */
19
+ export function layersUsed(repoRoot) {
20
+ const folder = path.join(repoRoot, '.starcistacks', 'images');
21
+ if (!fs.existsSync(folder))
22
+ return [];
23
+ const found = new Map();
24
+ for (const entry of fs.readdirSync(folder, { withFileTypes: true }).filter((item) => item.isDirectory()).sort((a, b) => (a.name < b.name ? -1 : 1))) {
25
+ const dockerfile = path.join(folder, entry.name, 'Dockerfile');
26
+ if (!fs.existsSync(dockerfile))
27
+ continue;
28
+ for (const line of fs.readFileSync(dockerfile, 'utf8').split(/\r?\n/u)) {
29
+ const from = /^\s*FROM\s+(?:--\S+\s+)*(\S+)/iu.exec(line)?.[1];
30
+ if (from?.startsWith(LAYER_PREFIX) !== true)
31
+ continue;
32
+ const reference = from;
33
+ const tag = reference.replace(/@sha256:[0-9a-f]+$/u, '');
34
+ const layer = tag.slice(LAYER_PREFIX.length).replace(/:.*$/u, '');
35
+ if (SHIPPED_LAYERS.includes(layer) && !found.has(reference))
36
+ found.set(reference, { layer, reference, tag: tag.includes(':') ? tag : `${tag}:latest`, dockerfile: path.relative(repoRoot, dockerfile).split(path.sep).join('/') });
37
+ }
38
+ }
39
+ return [...found.values()];
40
+ }
41
+ /** Whether the host holds `reference` or can pull it: the local store first, then the registry's manifest. */
42
+ async function available(ctx, use) {
43
+ if ((await ctx.spawn('docker', ['image', 'inspect', use.tag])).status === 0)
44
+ return true;
45
+ return (await ctx.spawn('docker', ['manifest', 'inspect', use.reference])).status === 0;
46
+ }
47
+ /** The lines of the plan a dry run prints for the layers the app builds on. */
48
+ export const layerPlan = (local) => layersUsed(local.repoRoot).map((use) => `plan runtime layer ${use.layer}: ${use.tag} is built from the shipped recipe when the host holds none and the registry has none`);
49
+ /** Builds every layer the app names that the host neither holds nor can pull; returns one line per layer. */
50
+ export async function ensureRuntimeLayers(ctx, local) {
51
+ const lines = [];
52
+ for (const use of layersUsed(local.repoRoot)) {
53
+ if (await available(ctx, use)) {
54
+ lines.push(`runtime layer ${use.layer}: ${use.tag} is available`);
55
+ continue;
56
+ }
57
+ const recipe = recipeDirectory(use.layer);
58
+ if (!fs.existsSync(path.join(recipe, 'Dockerfile')))
59
+ throw failure('STACK_LOCAL_RUNTIME_LAYER', `${use.dockerfile} builds on ${use.reference}, which is not published, and this package ships no recipe for ${use.layer}`);
60
+ const argv = ['docker', 'build', '-t', use.tag, recipe];
61
+ const built = ctx.heavy ? await ctx.heavy({ kind: 'build', label: `${local.project} runtime layer ${use.layer}`, cwd: local.repoRoot, argv, env: ctx.env }) : await ctx.spawn('docker', argv.slice(1), { cwd: local.repoRoot, env: ctx.env });
62
+ if (built.status !== 0)
63
+ throw failure('STACK_LOCAL_RUNTIME_LAYER', `docker build of the runtime layer ${use.layer} failed: ${(built.stderr.trim().split(/\r?\n/u)[0] ?? '').slice(0, 200)}`);
64
+ lines.push(`runtime layer ${use.layer}: ${use.reference} is not published; built ${use.tag} from the recipe this package ships`);
65
+ }
66
+ return lines;
67
+ }
@@ -0,0 +1,36 @@
1
+ /** The TypeScript compiler API of the app's own install, typed by the version this package develops against. */
2
+ type Ts = typeof import('typescript');
3
+ /** The refusal code of a held group. */
4
+ export declare const GROUP_HELD = "HFS_SYNC_GROUP_HELD";
5
+ /** The pseudo-group of the retired files a sync deletes. */
6
+ export declare const RETIRED_GROUP = "retired";
7
+ /** One write the guard judges: an app-relative path, its managed group, and its new text (null: the file is deleted). */
8
+ export interface GuardedWrite {
9
+ path: string;
10
+ group: string;
11
+ text: string | null;
12
+ }
13
+ /** A group sync does not write, with its files and the errors its writes would add. */
14
+ export interface HeldGroup {
15
+ group: string;
16
+ files: string[];
17
+ errors: string[];
18
+ }
19
+ /** What the guard answers: the held groups, and the sides it could not compile. */
20
+ export interface GuardVerdict {
21
+ held: HeldGroup[];
22
+ unchecked: string[];
23
+ }
24
+ /** Whether `file` (app-relative) is TypeScript source a side compiles. */
25
+ export declare const isGuardedSource: (file: string) => boolean;
26
+ /** The tsconfig that compiles an app-relative source: `be/tsconfig.json`, or the front-end app's or package's own; null when there is none. */
27
+ export declare function projectOf(root: string, file: string): string | null;
28
+ /**
29
+ * The groups `starci app sync --execute` holds back at `root`: every write of a TypeScript source is compiled, per side, with the side's own
30
+ * TypeScript (`loadTs`, the app's install by default); a side with no tsconfig compiles nothing, a side whose TypeScript is not installed is
31
+ * named in `unchecked`.
32
+ */
33
+ export declare function guardWrites(root: string, writes: readonly GuardedWrite[], loadTs?: (dir: string) => Ts | null): GuardVerdict;
34
+ /** The lines sync prints for a held group: what is held and why, the first errors, and what to do. */
35
+ export declare function heldLines(group: HeldGroup): string[];
36
+ export {};