@theford/runly 0.0.0-stage → 0.20.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 (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +157 -2
  3. package/dist/cli/client.d.ts +80 -0
  4. package/dist/cli/client.js +294 -0
  5. package/dist/cli/client.js.map +1 -0
  6. package/dist/cli/daemon-unit.d.ts +58 -0
  7. package/dist/cli/daemon-unit.js +289 -0
  8. package/dist/cli/daemon-unit.js.map +1 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +1482 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/install.d.ts +9 -0
  13. package/dist/cli/install.js +53 -0
  14. package/dist/cli/install.js.map +1 -0
  15. package/dist/cli/logs.d.ts +43 -0
  16. package/dist/cli/logs.js +232 -0
  17. package/dist/cli/logs.js.map +1 -0
  18. package/dist/cli/tether.d.ts +3 -0
  19. package/dist/cli/tether.js +65 -0
  20. package/dist/cli/tether.js.map +1 -0
  21. package/dist/cli/watchdog.d.ts +1 -0
  22. package/dist/cli/watchdog.js +81 -0
  23. package/dist/cli/watchdog.js.map +1 -0
  24. package/dist/core/builds.d.ts +20 -0
  25. package/dist/core/builds.js +93 -0
  26. package/dist/core/builds.js.map +1 -0
  27. package/dist/core/caller-env.d.ts +17 -0
  28. package/dist/core/caller-env.js +77 -0
  29. package/dist/core/caller-env.js.map +1 -0
  30. package/dist/core/diagnostics.d.ts +68 -0
  31. package/dist/core/diagnostics.js +34 -0
  32. package/dist/core/diagnostics.js.map +1 -0
  33. package/dist/core/env-vars.d.ts +35 -0
  34. package/dist/core/env-vars.js +32 -0
  35. package/dist/core/env-vars.js.map +1 -0
  36. package/dist/core/events.d.ts +10 -0
  37. package/dist/core/events.js +42 -0
  38. package/dist/core/events.js.map +1 -0
  39. package/dist/core/exec.d.ts +22 -0
  40. package/dist/core/exec.js +84 -0
  41. package/dist/core/exec.js.map +1 -0
  42. package/dist/core/journal.d.ts +227 -0
  43. package/dist/core/journal.js +395 -0
  44. package/dist/core/journal.js.map +1 -0
  45. package/dist/core/logs.d.ts +56 -0
  46. package/dist/core/logs.js +259 -0
  47. package/dist/core/logs.js.map +1 -0
  48. package/dist/core/manifest.d.ts +292 -0
  49. package/dist/core/manifest.js +179 -0
  50. package/dist/core/manifest.js.map +1 -0
  51. package/dist/core/namespaces.d.ts +7 -0
  52. package/dist/core/namespaces.js +59 -0
  53. package/dist/core/namespaces.js.map +1 -0
  54. package/dist/core/paths.d.ts +31 -0
  55. package/dist/core/paths.js +65 -0
  56. package/dist/core/paths.js.map +1 -0
  57. package/dist/core/policy.d.ts +94 -0
  58. package/dist/core/policy.js +122 -0
  59. package/dist/core/policy.js.map +1 -0
  60. package/dist/core/ports.d.ts +52 -0
  61. package/dist/core/ports.js +94 -0
  62. package/dist/core/ports.js.map +1 -0
  63. package/dist/core/presets.d.ts +25 -0
  64. package/dist/core/presets.js +86 -0
  65. package/dist/core/presets.js.map +1 -0
  66. package/dist/core/procscan.d.ts +111 -0
  67. package/dist/core/procscan.js +342 -0
  68. package/dist/core/procscan.js.map +1 -0
  69. package/dist/core/retention.d.ts +97 -0
  70. package/dist/core/retention.js +322 -0
  71. package/dist/core/retention.js.map +1 -0
  72. package/dist/core/sleep.d.ts +51 -0
  73. package/dist/core/sleep.js +99 -0
  74. package/dist/core/sleep.js.map +1 -0
  75. package/dist/core/tree-ledger.d.ts +39 -0
  76. package/dist/core/tree-ledger.js +92 -0
  77. package/dist/core/tree-ledger.js.map +1 -0
  78. package/dist/core/types.d.ts +21 -0
  79. package/dist/core/types.js +6 -0
  80. package/dist/core/types.js.map +1 -0
  81. package/dist/core/units.d.ts +20 -0
  82. package/dist/core/units.js +69 -0
  83. package/dist/core/units.js.map +1 -0
  84. package/dist/core/upkeep.d.ts +90 -0
  85. package/dist/core/upkeep.js +176 -0
  86. package/dist/core/upkeep.js.map +1 -0
  87. package/dist/core/util.d.ts +46 -0
  88. package/dist/core/util.js +168 -0
  89. package/dist/core/util.js.map +1 -0
  90. package/dist/core/version.d.ts +39 -0
  91. package/dist/core/version.js +144 -0
  92. package/dist/core/version.js.map +1 -0
  93. package/dist/core/worktree.d.ts +51 -0
  94. package/dist/core/worktree.js +309 -0
  95. package/dist/core/worktree.js.map +1 -0
  96. package/dist/daemon/budget.d.ts +155 -0
  97. package/dist/daemon/budget.js +288 -0
  98. package/dist/daemon/budget.js.map +1 -0
  99. package/dist/daemon/election.d.ts +9 -0
  100. package/dist/daemon/election.js +140 -0
  101. package/dist/daemon/election.js.map +1 -0
  102. package/dist/daemon/engine.d.ts +1703 -0
  103. package/dist/daemon/engine.js +5305 -0
  104. package/dist/daemon/engine.js.map +1 -0
  105. package/dist/daemon/index.d.ts +2 -0
  106. package/dist/daemon/index.js +412 -0
  107. package/dist/daemon/index.js.map +1 -0
  108. package/dist/daemon/proxy.d.ts +108 -0
  109. package/dist/daemon/proxy.js +615 -0
  110. package/dist/daemon/proxy.js.map +1 -0
  111. package/dist/daemon/supervisor.d.ts +101 -0
  112. package/dist/daemon/supervisor.js +493 -0
  113. package/dist/daemon/supervisor.js.map +1 -0
  114. package/dist/drivers/appliances.d.ts +11 -0
  115. package/dist/drivers/appliances.js +174 -0
  116. package/dist/drivers/appliances.js.map +1 -0
  117. package/dist/drivers/datastores.d.ts +144 -0
  118. package/dist/drivers/datastores.js +775 -0
  119. package/dist/drivers/datastores.js.map +1 -0
  120. package/dist/drivers/preview.d.ts +69 -0
  121. package/dist/drivers/preview.js +605 -0
  122. package/dist/drivers/preview.js.map +1 -0
  123. package/dist/drivers/types.d.ts +70 -0
  124. package/dist/drivers/types.js +13 -0
  125. package/dist/drivers/types.js.map +1 -0
  126. package/package.json +63 -4
  127. package/schema/backlot.schema.json +549 -0
  128. package/schema/runly.schema.json +556 -0
@@ -0,0 +1,179 @@
1
+ import { readFileSync, existsSync, realpathSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { parse } from 'yaml';
5
+ import { createRequire } from 'node:module';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { BrokerError, safeJoin } from './util.js';
8
+ /** The build line and its `when:` globs, whichever form the manifest used. */
9
+ export function buildOf(spec) {
10
+ if (spec.build === undefined)
11
+ return undefined;
12
+ if (typeof spec.build === 'string')
13
+ return { run: spec.build };
14
+ return { run: spec.build.run, when: spec.build.when, serial: spec.build.serial };
15
+ }
16
+ /** The output globs and how they are compared, whichever form the manifest used. */
17
+ export function outputsOf(spec) {
18
+ const o = spec.outputs;
19
+ if (o === undefined)
20
+ return { paths: [], compare: 'stat' };
21
+ if (typeof o === 'string')
22
+ return { paths: [o], compare: 'stat' };
23
+ if (Array.isArray(o))
24
+ return { paths: o, compare: 'stat' };
25
+ return { paths: o.paths, compare: o.compare ?? 'stat' };
26
+ }
27
+ /** A rule's declared `outputs:` as a list (empty when it declares none). */
28
+ export const upkeepOutputs = (rule) => rule.outputs === undefined ? [] : Array.isArray(rule.outputs) ? rule.outputs : [rule.outputs];
29
+ /**
30
+ * One shape for consumers regardless of which form the manifest used: `[]` when no
31
+ * login is declared, otherwise every declared login in order. A single-object
32
+ * manifest yields exactly one entry, so callers never branch on the manifest form.
33
+ */
34
+ export function normalizeLogins(spec) {
35
+ if (!spec)
36
+ return [];
37
+ return Array.isArray(spec) ? spec : [spec];
38
+ }
39
+ /**
40
+ * One-line warnings for manifest sections runly accepts but no longer acts on.
41
+ * The CLI prints them to stderr; the manifest still loads.
42
+ */
43
+ export function manifestDeprecations(manifest, file = 'runly.yml') {
44
+ const out = [];
45
+ if (manifest.checks !== undefined) {
46
+ out.push(`${file} declares 'checks:', which is ignored since 'runly run' was removed (decision 0032) — run your checks yourself, with 'runly ctx --env' for the environment`);
47
+ }
48
+ return out;
49
+ }
50
+ /** The manifest's file name for a message: the one actually loaded. */
51
+ export const manifestFileOf = (stack) => stack?.file ?? 'runly.yml';
52
+ const schemaPath = () => join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schema', 'runly.schema.json');
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ let validator;
55
+ function validate(data) {
56
+ if (!validator) {
57
+ // Loaded on first use: ajv and the schema compile cost ~100 ms, which a
58
+ // CLI verb paid on every invocation although the daemon validates the
59
+ // manifest it acts on anyway. ajv is CJS; the constructor lands on
60
+ // .default under real ESM interop.
61
+ const Ajv2020 = createRequire(import.meta.url)('ajv/dist/2020.js');
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ const AjvCtor = Ajv2020.default ?? Ajv2020;
64
+ const ajv = new AjvCtor({ allErrors: true });
65
+ validator = ajv.compile(JSON.parse(readFileSync(schemaPath(), 'utf8')));
66
+ }
67
+ if (!validator(data)) {
68
+ throw new BrokerError('work-error', `the runly manifest is invalid: ${JSON.stringify(validator.errors)}`, 'manifest');
69
+ }
70
+ }
71
+ /** Walk upward from cwd to the nearest manifest. */
72
+ /** runly.yml is canonical; backlot.yml and stack.yaml stay accepted so
73
+ * existing consumers survive the upgrade. When several exist, runly.yml wins —
74
+ * a rename, not a coin toss. */
75
+ export const MANIFEST_NAMES = ['runly.yml', 'backlot.yml', 'stack.yaml'];
76
+ function manifestIn(dir) {
77
+ for (const name of MANIFEST_NAMES) {
78
+ if (existsSync(join(dir, name)))
79
+ return join(dir, name);
80
+ }
81
+ return null;
82
+ }
83
+ export function canonicalDirectory(from) {
84
+ try {
85
+ return realpathSync(resolve(from));
86
+ }
87
+ catch {
88
+ throw new BrokerError('work-error', `cannot resolve project directory '${from}'`, 'manifest');
89
+ }
90
+ }
91
+ const IDENTITY_HASH_LENGTH = 8;
92
+ export function stackIdentity(name, root) {
93
+ return `${name}-${createHash('sha256').update(root).digest('base64url').slice(0, IDENTITY_HASH_LENGTH)}`;
94
+ }
95
+ export function findStackRoot(from) {
96
+ let dir = canonicalDirectory(from);
97
+ for (;;) {
98
+ if (manifestIn(dir))
99
+ return dir;
100
+ const parent = dirname(dir);
101
+ if (parent === dir) {
102
+ throw new BrokerError('work-error', `no runly.yml (or backlot.yml / stack.yaml) found from ${from} upward`, 'manifest');
103
+ }
104
+ dir = parent;
105
+ }
106
+ }
107
+ /**
108
+ * Find, read and validate the manifest from `from` upward. `validate: false`
109
+ * skips the schema check — only for a CLI-side read whose result the daemon
110
+ * re-reads and validates before acting on it (the deprecation warnings).
111
+ */
112
+ export function loadStack(from, opts = {}) {
113
+ const root = findStackRoot(from);
114
+ const file = manifestIn(root);
115
+ if (!file)
116
+ throw new BrokerError('work-error', `no runly.yml (or backlot.yml / stack.yaml) in ${root}`, 'manifest');
117
+ const manifest = parse(readFileSync(file, 'utf8'));
118
+ if (opts.validate !== false)
119
+ validate(manifest);
120
+ // A path that escapes the worktree is refused at load, whether or not an
121
+ // upkeep rule ever makes runly read it: it is never a legitimate source file.
122
+ for (const inc of manifest.sync?.include ?? [])
123
+ safeJoin(root, inc, 'sync.include');
124
+ checkCopiesOnly(manifest, basename(file));
125
+ // Identity = absolute root + declared name; filesystem-safe. Hash the WHOLE
126
+ // path: slicing base64url(root) kept only the last ~6 bytes, so sibling
127
+ // worktrees like agent-1/myapp and agent-2/myapp collided into one pool.
128
+ const id = stackIdentity(manifest.name, root);
129
+ return { manifest, root, id, file: basename(file) };
130
+ }
131
+ /** Every string in `value`, with the manifest path it sits at (`services.web.ready.cmd`). */
132
+ function* stringsIn(value, path) {
133
+ if (typeof value === 'string')
134
+ yield [path, value];
135
+ else if (Array.isArray(value))
136
+ for (const [i, v] of value.entries())
137
+ yield* stringsIn(v, `${path}[${i}]`);
138
+ else if (value && typeof value === 'object')
139
+ for (const [k, v] of Object.entries(value))
140
+ yield* stringsIn(v, path ? `${path}.${k}` : k);
141
+ }
142
+ /**
143
+ * A `copies_only` datastore (decision 0039) has no environment namespace, so
144
+ * nothing templated against an environment can name one: any field outside
145
+ * `datastores:` that templates it — a service's run, build, env or ready:,
146
+ * `auth.token`, a preview setting — is refused at load, naming the field and
147
+ * the alternative.
148
+ */
149
+ function checkCopiesOnly(manifest, file) {
150
+ const only = Object.entries(manifest.datastores ?? {}).filter(([, spec]) => spec.copies_only === true).map(([ds]) => ds);
151
+ if (only.length === 0)
152
+ return;
153
+ const rest = Object.fromEntries(Object.entries(manifest).filter(([k]) => k !== 'datastores'));
154
+ for (const ds of only) {
155
+ const escaped = ds.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
156
+ const ref = new RegExp(`\\{\\{\\s*datastores\\.${escaped}\\.`);
157
+ for (const [where, text] of stringsIn(rest, '')) {
158
+ if (!ref.test(text))
159
+ continue;
160
+ const svc = /^services\.([^.[]+)\./.exec(where)?.[1];
161
+ throw new BrokerError('work-error', `${svc !== undefined ? `service '${svc}'` : where} templates datastore '${ds}', which is copies_only in ${file}${svc !== undefined ? ` (at ${where})` : ''} — an environment never has one; take a copy with 'runly db new ${ds}' (or 'runly db with ${ds} -- <cmd>'), or drop copies_only`, 'manifest');
162
+ }
163
+ }
164
+ }
165
+ /** The datastores an environment provisions: every declared one except `copies_only` (decision 0039). */
166
+ export function envDatastoreNames(manifest) {
167
+ return Object.entries(manifest.datastores ?? {}).filter(([, spec]) => spec.copies_only !== true).map(([name]) => name);
168
+ }
169
+ /** The declared default preset, if any — one value, whichever form the manifest used. */
170
+ export function declaredDefaultPreset(ds) {
171
+ const d = ds.default_preset;
172
+ if (d === undefined)
173
+ return undefined;
174
+ return typeof d === 'string' ? d : (d.session ?? d.run);
175
+ }
176
+ export function defaultPreset(ds) {
177
+ return declaredDefaultPreset(ds) ?? ds.presets?.[0] ?? 'default';
178
+ }
179
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/core/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** Postgres's identifier limit; every truncated namespace is exactly this long. */
2
+ export declare const NS_LIMIT = 63;
3
+ export declare function recordedNamespaces(): Map<string, string>;
4
+ /** Remember that `ns` (a truncated name) belongs to `stack`. A short name needs no record: its prefix says it. */
5
+ export declare function recordNamespace(ns: string, stack: string): void;
6
+ /** Forget `ns` once it is dropped. */
7
+ export declare function forgetNamespace(ns: string): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Server-side namespaces whose names do not carry their stack (decision 0037).
3
+ *
4
+ * A datastore namespace is `backlot_<env or stack>_<name>`, cut to Postgres's
5
+ * 63 bytes with a hash at the end. For a long stack id the cut lands inside
6
+ * the stack id, so `pool doctor` could not tell the namespace was this state
7
+ * root's from its name, called it foreign and never dropped it. Every
8
+ * namespace that is cut is therefore recorded here, with its stack, before it
9
+ * is created; doctor reads the record next to the name prefixes.
10
+ */
11
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { stateRoot } from './paths.js';
14
+ /** Postgres's identifier limit; every truncated namespace is exactly this long. */
15
+ export const NS_LIMIT = 63;
16
+ const file = () => join(stateRoot(), 'namespaces.json');
17
+ export function recordedNamespaces() {
18
+ try {
19
+ const raw = JSON.parse(readFileSync(file(), 'utf8'));
20
+ return new Map(Object.entries(raw).filter(([, v]) => typeof v === 'string'));
21
+ }
22
+ catch {
23
+ return new Map();
24
+ }
25
+ }
26
+ function save(all) {
27
+ mkdirSync(stateRoot(), { recursive: true });
28
+ const tmp = `${file()}.${process.pid}.tmp`;
29
+ writeFileSync(tmp, JSON.stringify(Object.fromEntries(all)));
30
+ renameSync(tmp, file());
31
+ }
32
+ /** Remember that `ns` (a truncated name) belongs to `stack`. A short name needs no record: its prefix says it. */
33
+ export function recordNamespace(ns, stack) {
34
+ if (ns.length < NS_LIMIT)
35
+ return;
36
+ const all = recordedNamespaces();
37
+ if (all.get(ns) === stack)
38
+ return;
39
+ all.set(ns, stack);
40
+ try {
41
+ save(all);
42
+ }
43
+ catch {
44
+ /* best effort: doctor then reports it as foreign, as before */
45
+ }
46
+ }
47
+ /** Forget `ns` once it is dropped. */
48
+ export function forgetNamespace(ns) {
49
+ const all = recordedNamespaces();
50
+ if (!all.delete(ns))
51
+ return;
52
+ try {
53
+ save(all);
54
+ }
55
+ catch {
56
+ /* best effort */
57
+ }
58
+ }
59
+ //# sourceMappingURL=namespaces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"namespaces.js","sourceRoot":"","sources":["../../src/core/namespaces.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,mFAAmF;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC;AAE3B,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,iBAAiB,CAAC,CAAC;AAExD,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAA2B,CAAC;QAC/E,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,IAAI,CAAC,GAAwB;IACpC,SAAS,CAAC,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,GAAG,IAAI,EAAE,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IAC3C,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5D,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;AAC1B,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,eAAe,CAAC,EAAU,EAAE,KAAa;IACvD,IAAI,EAAE,CAAC,MAAM,GAAG,QAAQ;QAAE,OAAO;IACjC,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;IACjC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,KAAK;QAAE,OAAO;IAClC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACnB,IAAI,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;AACH,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;IACjC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAAE,OAAO;IAC5B,IAAI,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,iBAAiB;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Per-machine state root (decision 0009). BACKLOT_STATE_DIR overrides — the
3
+ * isolation knob every integration test uses.
4
+ *
5
+ * Created 0700 and chmod-enforced every call: the daemon socket lives here and
6
+ * has NO application-level auth (README "Security model" — filesystem
7
+ * permissions ARE the auth), so no other local user may traverse in. We chmod
8
+ * even when the dir already exists, to repair a too-open pre-existing dir
9
+ * (e.g. one made before this hardening, or via a loose umask).
10
+ */
11
+ export declare function stateRoot(): string;
12
+ export declare const socketPath: () => string;
13
+ export declare const pidPath: () => string;
14
+ /**
15
+ * The singleton-election lock. Separate from daemon.pid, which stays a bare
16
+ * number for anything that reads it; this one carries the identity needed to
17
+ * tell a live holder from a crashed one.
18
+ */
19
+ export declare const lockPath: () => string;
20
+ export declare const journalPath: () => string;
21
+ export declare const envsRoot: () => string;
22
+ export declare const templatesRoot: () => string;
23
+ /** Private directories of `runly db` copies whose namespace is a file (sqlite), one per copy (decision 0034). */
24
+ export declare const dbCopiesRoot: () => string;
25
+ /**
26
+ * Per-worktree state (decision 0032): the trigger-file hash cache and the
27
+ * worktree's upkeep ledger. It describes the caller's worktree, which
28
+ * every environment of that stack runs in, so it is keyed by stack id rather
29
+ * than by environment — and lives here, never inside the worktree.
30
+ */
31
+ export declare const worktreesRoot: () => string;
@@ -0,0 +1,65 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { mkdirSync, chmodSync } from 'node:fs';
4
+ /**
5
+ * Per-machine state root (decision 0009). BACKLOT_STATE_DIR overrides — the
6
+ * isolation knob every integration test uses.
7
+ *
8
+ * Created 0700 and chmod-enforced every call: the daemon socket lives here and
9
+ * has NO application-level auth (README "Security model" — filesystem
10
+ * permissions ARE the auth), so no other local user may traverse in. We chmod
11
+ * even when the dir already exists, to repair a too-open pre-existing dir
12
+ * (e.g. one made before this hardening, or via a loose umask).
13
+ */
14
+ export function stateRoot() {
15
+ const root = process.env.BACKLOT_STATE_DIR ??
16
+ join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'backlot');
17
+ mkdirSync(root, { recursive: true, mode: 0o700 });
18
+ try {
19
+ chmodSync(root, 0o700);
20
+ }
21
+ catch {
22
+ /* not the owner — leave it; the socket chmod below is the real gate */
23
+ }
24
+ return root;
25
+ }
26
+ /**
27
+ * AF_UNIX `sun_path` is 104 bytes on macOS and 108 on Linux (including the
28
+ * NUL); we enforce the tighter leg. Over the limit the OS does NOT error — it
29
+ * silently truncates (verified on macOS), and because client and daemon
30
+ * truncate identically everything appears to work while actually binding a
31
+ * socket that collides with any other state dir sharing the 103-byte prefix.
32
+ */
33
+ const SUN_PATH_MAX = 103;
34
+ export const socketPath = () => {
35
+ const sock = join(stateRoot(), 'daemon.sock');
36
+ const bytes = Buffer.byteLength(sock);
37
+ if (bytes > SUN_PATH_MAX) {
38
+ // "socket" in the message keeps classifyClientError reading this as
39
+ // infra-error — a bad state dir is never fixed by recycling an environment.
40
+ throw new Error(`unix socket path '${sock}' is ${bytes} bytes — over the ${SUN_PATH_MAX}-byte AF_UNIX sun_path limit ` +
41
+ `(104 on macOS incl. NUL), where the OS silently truncates and state dirs collide. ` +
42
+ `Point BACKLOT_STATE_DIR at a shorter path.`);
43
+ }
44
+ return sock;
45
+ };
46
+ export const pidPath = () => join(stateRoot(), 'daemon.pid');
47
+ /**
48
+ * The singleton-election lock. Separate from daemon.pid, which stays a bare
49
+ * number for anything that reads it; this one carries the identity needed to
50
+ * tell a live holder from a crashed one.
51
+ */
52
+ export const lockPath = () => join(stateRoot(), 'daemon.lock');
53
+ export const journalPath = () => join(stateRoot(), 'journal.db');
54
+ export const envsRoot = () => join(stateRoot(), 'envs');
55
+ export const templatesRoot = () => join(stateRoot(), 'templates');
56
+ /** Private directories of `runly db` copies whose namespace is a file (sqlite), one per copy (decision 0034). */
57
+ export const dbCopiesRoot = () => join(stateRoot(), 'dbs');
58
+ /**
59
+ * Per-worktree state (decision 0032): the trigger-file hash cache and the
60
+ * worktree's upkeep ledger. It describes the caller's worktree, which
61
+ * every environment of that stack runs in, so it is keyed by stack id rather
62
+ * than by environment — and lives here, never inside the worktree.
63
+ */
64
+ export const worktreesRoot = () => join(stateRoot(), 'worktrees');
65
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS;IACvB,MAAM,IAAI,GACR,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAC7B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC;IACpF,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,IAAI,CAAC;QACH,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;IACzE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB,MAAM,CAAC,MAAM,UAAU,GAAG,GAAW,EAAE;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,KAAK,GAAG,YAAY,EAAE,CAAC;QACzB,oEAAoE;QACpE,4EAA4E;QAC5E,MAAM,IAAI,KAAK,CACb,qBAAqB,IAAI,QAAQ,KAAK,qBAAqB,YAAY,+BAA+B;YACpG,oFAAoF;YACpF,4CAA4C,CAC/C,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AACF,MAAM,CAAC,MAAM,OAAO,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACrE;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;AACvE,MAAM,CAAC,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACzE,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,WAAW,CAAC,CAAC;AAC1E,iHAAiH;AACjH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,KAAK,CAAC,CAAC;AACnE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,WAAW,CAAC,CAAC"}
@@ -0,0 +1,94 @@
1
+ export interface Policy {
2
+ /** Machine-wide ceiling across ALL stacks (the memory heuristic is per host). */
3
+ poolMaxTotal: number;
4
+ sessionTtlMs: number;
5
+ idleTtlMs: number;
6
+ waitMs: number;
7
+ /** Retention knobs (task: disk sweep). */
8
+ logCapBytes: number;
9
+ /** Templates kept per stack/datastore/preset beyond the referenced ones (decision 0037). */
10
+ templatesKeep: number;
11
+ /** How long an unreferenced, superseded template survives before it is dropped (decision 0037). */
12
+ templateGraceMs: number;
13
+ /** A service with no client bytes and no runly verb for this long is stopped (decision 0035). */
14
+ serviceIdleMs: number;
15
+ /** A dead tether is believed only after this long (decision 0035). */
16
+ tetherGraceMs: number;
17
+ /**
18
+ * An environment nobody leases is torn down (services, data, ports) after
19
+ * this long without activity (0.19, BACKLOT_UNLEASED_TTL, default 24h,
20
+ * `off` never). Templates and the worktree's records stay, so the next
21
+ * `up` there restores rather than bakes and skips fresh upkeep.
22
+ */
23
+ unleasedTtlMs: number;
24
+ /** The server-wide load budget (decision 0036). */
25
+ budget: Budget;
26
+ }
27
+ /**
28
+ * The load budget (decision 0036): what runly may commit on this box across
29
+ * every stack. `enabled: false` (BACKLOT_BUDGET=off) admits everything.
30
+ */
31
+ export interface Budget {
32
+ enabled: boolean;
33
+ /** Declared memory runly may have committed at once (running services + builds in flight). */
34
+ memoryBytes: number;
35
+ /** Declared cores runly may have committed at once. */
36
+ cpu: number;
37
+ /** Linux: MemAvailable must stay above this after a start (headroom for non-runly work). */
38
+ reserveBytes: number;
39
+ /**
40
+ * Linux with PSI: a start waits while runnable work stalled on the CPU at
41
+ * least this percent of the time over both the last 10 s and the last 60 s
42
+ * (`/proc/pressure/cpu` "some"). 100 or more switches the gate off.
43
+ */
44
+ cpuPressure: number;
45
+ /** Without PSI (macOS, old kernels): a start waits while the 1-minute load average exceeds this many times the core count. */
46
+ loadPerCore: number;
47
+ /** How long an `up` waits in the queue before it fails. */
48
+ waitMs: number;
49
+ /** How many requests may wait at once; the next one is refused. */
50
+ maxQueue: number;
51
+ /** What an undeclared service costs while running, and while building. */
52
+ defaultRun: {
53
+ memoryBytes: number;
54
+ cpu: number;
55
+ };
56
+ defaultBuild: {
57
+ memoryBytes: number;
58
+ cpu: number;
59
+ };
60
+ }
61
+ interface ConfigFile {
62
+ serviceIdleMs?: number;
63
+ tetherGraceMs?: number;
64
+ /** A duration (`24h`, `90m`, seconds as a number) or `off`. */
65
+ unleasedTtl?: string | number;
66
+ templateGraceMs?: number;
67
+ budget?: Partial<Budget> & {
68
+ memory?: string | number;
69
+ reserve?: string | number;
70
+ };
71
+ poolMaxTotal?: number;
72
+ sessionTtlMs?: number;
73
+ idleTtlMs?: number;
74
+ waitMs?: number;
75
+ logCapBytes?: number;
76
+ templatesKeep?: number;
77
+ }
78
+ /**
79
+ * The machine-wide cap on environment ROWS (BACKLOT_POOL_MAX_TOTAL).
80
+ *
81
+ * Since 0.16 (decision 0036) the load budget bounds what RUNS, so this cap
82
+ * only bounds what is HELD — ports, data directories, database namespaces —
83
+ * by environments that may be idle-stopped and cost nothing running:
84
+ * 2 x cores, clamped to [4, 64] (32 on a 16-core box). With the budget
85
+ * switched off (BACKLOT_BUDGET=off) nothing else bounds concurrent load, and
86
+ * the pre-0.16 heuristic applies: min(cores/2, memGB/4) clamped to [2, 8].
87
+ */
88
+ export declare function poolMaxHeuristic(budgetEnabled?: boolean): number;
89
+ /** The pre-0.16 load-bounding heuristic, used when the budget is off. */
90
+ export declare function legacyPoolMaxHeuristic(): number;
91
+ /** The defaults leave headroom for work that is not runly's (decision 0036). */
92
+ export declare function budgetPolicy(f?: ConfigFile): Budget;
93
+ export declare function policy(): Policy;
94
+ export {};
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Engine policy — NEVER repo knowledge (the manifest carries none of this).
3
+ * Precedence per knob: env var > $STATE_DIR/config.json > heuristic/default.
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { cpus, totalmem } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { stateRoot } from './paths.js';
9
+ import { parseDuration, parseSize } from './units.js';
10
+ function configFile() {
11
+ try {
12
+ return JSON.parse(readFileSync(join(stateRoot(), 'config.json'), 'utf8'));
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ /**
19
+ * The machine-wide cap on environment ROWS (BACKLOT_POOL_MAX_TOTAL).
20
+ *
21
+ * Since 0.16 (decision 0036) the load budget bounds what RUNS, so this cap
22
+ * only bounds what is HELD — ports, data directories, database namespaces —
23
+ * by environments that may be idle-stopped and cost nothing running:
24
+ * 2 x cores, clamped to [4, 64] (32 on a 16-core box). With the budget
25
+ * switched off (BACKLOT_BUDGET=off) nothing else bounds concurrent load, and
26
+ * the pre-0.16 heuristic applies: min(cores/2, memGB/4) clamped to [2, 8].
27
+ */
28
+ export function poolMaxHeuristic(budgetEnabled = budgetOn()) {
29
+ if (budgetEnabled)
30
+ return Math.max(4, Math.min(64, (cpus().length || 1) * 2));
31
+ return legacyPoolMaxHeuristic();
32
+ }
33
+ /** The pre-0.16 load-bounding heuristic, used when the budget is off. */
34
+ export function legacyPoolMaxHeuristic() {
35
+ const byCores = Math.floor(cpus().length / 2);
36
+ const byMem = Math.floor(totalmem() / (4 * 1024 ** 3));
37
+ return Math.max(2, Math.min(8, Math.min(byCores || 1, byMem || 1)));
38
+ }
39
+ function budgetOn() {
40
+ const v = (process.env.BACKLOT_BUDGET ?? '').trim().toLowerCase();
41
+ if (v === 'off' || v === '0' || v === 'false')
42
+ return false;
43
+ return configFile().budget?.enabled !== false;
44
+ }
45
+ const num = (envVar, fileVal, fallback) => {
46
+ const e = process.env[envVar];
47
+ if (e !== undefined && e !== '')
48
+ return Number(e);
49
+ if (fileVal !== undefined)
50
+ return fileVal;
51
+ return fallback;
52
+ };
53
+ /** A duration knob: env var or config value as `24h`/`90m`/seconds, or `off` (Infinity). */
54
+ const duration = (envVar, fileVal, fallback) => {
55
+ const e = process.env[envVar];
56
+ const fromEnv = e !== undefined && e !== '' ? parseDuration(e) : undefined;
57
+ if (fromEnv !== undefined)
58
+ return fromEnv;
59
+ const fromFile = fileVal !== undefined ? parseDuration(fileVal) : undefined;
60
+ return fromFile ?? fallback;
61
+ };
62
+ /** A size knob: env var or config value as bytes or `2G`. */
63
+ const size = (envVar, fileVal, fallback) => {
64
+ const e = process.env[envVar];
65
+ if (e !== undefined && e !== '')
66
+ return parseSize(e) ?? fallback;
67
+ if (fileVal !== undefined)
68
+ return parseSize(fileVal) ?? fallback;
69
+ return fallback;
70
+ };
71
+ /** The defaults leave headroom for work that is not runly's (decision 0036). */
72
+ export function budgetPolicy(f = configFile()) {
73
+ const b = f.budget ?? {};
74
+ const total = totalmem();
75
+ const cores = cpus().length || 1;
76
+ const off = (process.env.BACKLOT_BUDGET ?? '').toLowerCase();
77
+ return {
78
+ enabled: !(off === 'off' || off === '0' || off === 'false') && b.enabled !== false,
79
+ memoryBytes: size('BACKLOT_BUDGET_MEMORY', b.memory ?? b.memoryBytes, Math.floor(total * 0.7)),
80
+ cpu: num('BACKLOT_BUDGET_CPU', b.cpu, cores * 1.5),
81
+ reserveBytes: size('BACKLOT_BUDGET_RESERVE', b.reserve ?? b.reserveBytes, Math.max(2 * 1024 ** 3, Math.floor(total * 0.1))),
82
+ // Calibrated on a 16-core box shared by ~10 agents: a load of 25-43 (the
83
+ // old 2 x cores gate) held no-op ups for minutes while PSI "some" sat at
84
+ // 20-40%, i.e. the CPUs still had room. 70% over 10 s AND 60 s is a box
85
+ // that is genuinely saturated now and has been for a minute.
86
+ cpuPressure: num('BACKLOT_BUDGET_CPU_PRESSURE', b.cpuPressure, 70),
87
+ loadPerCore: num('BACKLOT_BUDGET_LOAD_PER_CORE', b.loadPerCore, 4),
88
+ waitMs: num('BACKLOT_BUDGET_WAIT_MS', b.waitMs, 10 * 60_000),
89
+ maxQueue: num('BACKLOT_BUDGET_MAX_QUEUE', b.maxQueue, 64),
90
+ defaultRun: { memoryBytes: 512 * 1024 ** 2, cpu: 0.5 },
91
+ defaultBuild: { memoryBytes: 1024 ** 3, cpu: 1 },
92
+ };
93
+ }
94
+ export function policy() {
95
+ const f = configFile();
96
+ const idleTtlMs = num('BACKLOT_IDLE_TTL_MS', f.idleTtlMs, 30 * 60_000);
97
+ return {
98
+ // The machine-wide ceiling: the heuristic is derived from this host's cores
99
+ // and memory, so it is a budget for the host, not per project. (There is no
100
+ // per-stack ceiling: a worktree has exactly one environment, decision 0032.)
101
+ // Raise it deliberately if the host can take it.
102
+ poolMaxTotal: num('BACKLOT_POOL_MAX_TOTAL', f.poolMaxTotal, poolMaxHeuristic()),
103
+ // There is no data-only ceiling any more (decision 0034): a database
104
+ // without an application is a `runly db` copy, which is not an
105
+ // environment and answers to no pool cap. BACKLOT_POOL_MAX_DATA_ONLY and
106
+ // `poolMaxDataOnly` are ignored.
107
+ sessionTtlMs: num('BACKLOT_LEASE_TTL_MS', f.sessionTtlMs, 30 * 60_000),
108
+ idleTtlMs,
109
+ waitMs: num('BACKLOT_WAIT_MS', f.waitMs, 60_000),
110
+ // One rotation per file (decision 0038): a service keeps up to twice this.
111
+ logCapBytes: num('BACKLOT_LOG_CAP_BYTES', f.logCapBytes, 20 * 1024 * 1024),
112
+ // Per stack/datastore/preset now (decision 0037), not per stack: the
113
+ // newest template of each, plus every one an environment or copy uses.
114
+ templatesKeep: num('BACKLOT_TEMPLATES_KEEP', f.templatesKeep, 1),
115
+ templateGraceMs: num('BACKLOT_TEMPLATE_GRACE_MS', f.templateGraceMs, 60 * 60_000),
116
+ serviceIdleMs: num('BACKLOT_SERVICE_IDLE_MS', f.serviceIdleMs, 10 * 60_000),
117
+ tetherGraceMs: num('BACKLOT_TETHER_GRACE_MS', f.tetherGraceMs, 60_000),
118
+ unleasedTtlMs: duration('BACKLOT_UNLEASED_TTL', f.unleasedTtl, 24 * 3_600_000),
119
+ budget: budgetPolicy(f),
120
+ };
121
+ }
122
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/core/policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Is `port` genuinely free?
3
+ *
4
+ * Binding only 127.0.0.1 misses a foreign process listening on the WILDCARD
5
+ * address: on Linux that bind collides and reports busy, but on macOS it
6
+ * succeeds, so the "port occupied by a foreign process" guard never fired
7
+ * there and the service started into a port someone else already held.
8
+ * Probing the wildcard as well is what makes the two platforms agree.
9
+ */
10
+ export declare function probeFree(port: number): Promise<boolean>;
11
+ /**
12
+ * Port blocks (decision 0033). Three disjoint blocks, all below every common
13
+ * OS ephemeral range (Linux 32768–60999, macOS/BSD 49152–65535), so an
14
+ * outbound connection's source port can never sit on a port runly is about to
15
+ * hand out:
16
+ *
17
+ * - PUBLIC 20000–29999: what users, child services, `ctx` and tunnels see. The
18
+ * daemon's proxy holds each one for the environment's lifetime.
19
+ * - INTERNAL 30000–31999: where a service actually listens, chosen fresh at
20
+ * every start. Only the proxy and runly's own readiness probes connect here.
21
+ * - TUNNEL 32000–32767: tailnet HTTPS ports a preview publisher derives.
22
+ *
23
+ * Each is overridable (`BACKLOT_PORT_RANGE`, `BACKLOT_INTERNAL_PORT_RANGE`,
24
+ * `BACKLOT_TUNNEL_PORT_RANGE`, form `LO-HI`) for a box whose own layout
25
+ * collides; the daemon logs a warning when a block overlaps the ephemeral range
26
+ * it reads from /proc.
27
+ */
28
+ export interface PortBlock {
29
+ lo: number;
30
+ hi: number;
31
+ }
32
+ export declare const DEFAULT_PUBLIC_BLOCK: PortBlock;
33
+ export declare const DEFAULT_INTERNAL_BLOCK: PortBlock;
34
+ export declare const DEFAULT_TUNNEL_BLOCK: PortBlock;
35
+ export declare const publicBlock: () => PortBlock;
36
+ export declare const internalBlock: () => PortBlock;
37
+ export declare const tunnelBlock: () => PortBlock;
38
+ export declare const inBlock: (port: number, block: PortBlock) => boolean;
39
+ /**
40
+ * The OS ephemeral (outbound source-port) range. Linux publishes it; other
41
+ * platforms get the IANA range their kernels use by default.
42
+ */
43
+ export declare function ephemeralRange(): PortBlock;
44
+ /** Human-readable overlaps of the configured blocks with each other and the ephemeral range. */
45
+ export declare function blockConflicts(): string[];
46
+ /**
47
+ * The first port of `block` that is not in `taken`, not ephemeral, and probes
48
+ * free, scanning from a random offset so concurrent daemons (and a daemon
49
+ * restarted with an empty memory) do not all start at the bottom. Returns
50
+ * undefined when the block has nothing left — the caller names the block.
51
+ */
52
+ export declare function allocateInBlock(block: PortBlock, taken: Set<number>, probe?: (port: number) => Promise<boolean>): Promise<number | undefined>;