@aopslabs/aops 0.2.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 (171) hide show
  1. package/LICENSE +19 -0
  2. package/NOTICE +7 -0
  3. package/README.md +203 -0
  4. package/assets/agent-assets/core/SKILL.md +49 -0
  5. package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
  6. package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
  7. package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
  8. package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
  9. package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
  10. package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
  11. package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
  12. package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
  13. package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
  14. package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
  15. package/assets/agent-assets/core/references/view/SKILL.md +58 -0
  16. package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
  17. package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
  18. package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
  19. package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
  20. package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
  21. package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
  22. package/assets/agent-assets/core/user-guides/docman.md +162 -0
  23. package/assets/agent-assets/core/user-guides/projectman.md +212 -0
  24. package/assets/agent-assets/core/user-guides/sys.md +94 -0
  25. package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
  26. package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
  27. package/assets/r6-remote-session-baseline.json +19 -0
  28. package/assets/skills/aops-install/SKILL.md +231 -0
  29. package/assets/skills/aops-install/agents/openai.yaml +4 -0
  30. package/dist/commands/activity.js +352 -0
  31. package/dist/commands/agent-profile.js +653 -0
  32. package/dist/commands/agent.js +710 -0
  33. package/dist/commands/agents-md.js +125 -0
  34. package/dist/commands/api.js +84 -0
  35. package/dist/commands/archive.js +873 -0
  36. package/dist/commands/artifact.js +535 -0
  37. package/dist/commands/assets.js +1414 -0
  38. package/dist/commands/auth/device-login.js +1 -0
  39. package/dist/commands/auth/legacy-export.js +177 -0
  40. package/dist/commands/auth/login.js +426 -0
  41. package/dist/commands/chat.js +1363 -0
  42. package/dist/commands/chatv3.js +1684 -0
  43. package/dist/commands/checkpoint.js +549 -0
  44. package/dist/commands/commercial-artifact.js +475 -0
  45. package/dist/commands/commercial-license.js +594 -0
  46. package/dist/commands/community-auth.js +62 -0
  47. package/dist/commands/community-cockpit.js +185 -0
  48. package/dist/commands/community-console.js +109 -0
  49. package/dist/commands/community-doctor.js +335 -0
  50. package/dist/commands/community-server.js +2062 -0
  51. package/dist/commands/community-setup.js +284 -0
  52. package/dist/commands/discuss.js +1448 -0
  53. package/dist/commands/doc.js +5219 -0
  54. package/dist/commands/experience.js +922 -0
  55. package/dist/commands/host.js +1127 -0
  56. package/dist/commands/init.js +118 -0
  57. package/dist/commands/memory.js +3129 -0
  58. package/dist/commands/mission.js +562 -0
  59. package/dist/commands/official-catalog.js +134 -0
  60. package/dist/commands/plan.js +84 -0
  61. package/dist/commands/playbook.js +415 -0
  62. package/dist/commands/pm/context.js +6 -0
  63. package/dist/commands/pm/index.js +701 -0
  64. package/dist/commands/pm/projectman.js +6266 -0
  65. package/dist/commands/project.js +1080 -0
  66. package/dist/commands/prompt.js +1024 -0
  67. package/dist/commands/repo-sync.js +1907 -0
  68. package/dist/commands/resource.js +527 -0
  69. package/dist/commands/skill.js +1211 -0
  70. package/dist/commands/start-disciplines.js +510 -0
  71. package/dist/commands/start.js +1343 -0
  72. package/dist/commands/target.js +309 -0
  73. package/dist/commands/version.js +42 -0
  74. package/dist/commands/view.js +1852 -0
  75. package/dist/launchers/cockpit.js +28 -0
  76. package/dist/launchers/options.js +74 -0
  77. package/dist/launchers/server.js +41 -0
  78. package/dist/lib/agent-assets/envelope.js +83 -0
  79. package/dist/lib/agent-assets/gateway.js +15 -0
  80. package/dist/lib/agent-assets/guards.js +23 -0
  81. package/dist/lib/agent-assets/hosted-discovery.js +148 -0
  82. package/dist/lib/agent-assets/hosted-package-input.js +154 -0
  83. package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
  84. package/dist/lib/agent-assets/native-fs.js +589 -0
  85. package/dist/lib/agent-assets/package-manifest.js +324 -0
  86. package/dist/lib/agent-assets/portable-path.js +67 -0
  87. package/dist/lib/agent-assets/release-input.js +177 -0
  88. package/dist/lib/agent-assets/roots.js +45 -0
  89. package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
  90. package/dist/lib/agent-assets/runtime-targets.js +50 -0
  91. package/dist/lib/agent-assets/store-reader.js +1212 -0
  92. package/dist/lib/agent-assets/store-types.js +1 -0
  93. package/dist/lib/agent-assets/store-writer.js +1484 -0
  94. package/dist/lib/agent-assets/types.js +1 -0
  95. package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
  96. package/dist/lib/commercial-final-readiness.js +504 -0
  97. package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
  98. package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
  99. package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
  100. package/dist/lib/community-client-contract.js +164 -0
  101. package/dist/lib/community-cockpit-lifecycle.js +601 -0
  102. package/dist/lib/community-command-abort.js +59 -0
  103. package/dist/lib/community-diagnostic.js +237 -0
  104. package/dist/lib/community-docker-adapter.js +498 -0
  105. package/dist/lib/community-home.js +31 -0
  106. package/dist/lib/community-instance-contract.js +148 -0
  107. package/dist/lib/community-instance-layout.js +97 -0
  108. package/dist/lib/community-lifecycle.js +1631 -0
  109. package/dist/lib/community-migration-snapshot.js +770 -0
  110. package/dist/lib/community-native-application-recovery.js +425 -0
  111. package/dist/lib/community-native-child.js +240 -0
  112. package/dist/lib/community-native-database-recovery.js +456 -0
  113. package/dist/lib/community-native-lifecycle.js +2458 -0
  114. package/dist/lib/community-native-migration.js +339 -0
  115. package/dist/lib/community-native-postgres.js +596 -0
  116. package/dist/lib/community-operation-journal.js +773 -0
  117. package/dist/lib/community-operation-lock.js +596 -0
  118. package/dist/lib/community-release-resolver.js +291 -0
  119. package/dist/lib/community-release-verifier.js +475 -0
  120. package/dist/lib/community-repo-discovery.js +252 -0
  121. package/dist/lib/community-setup-server-env.js +203 -0
  122. package/dist/lib/external-url.js +110 -0
  123. package/dist/lib/official-catalog-gateway.js +108 -0
  124. package/dist/lib/official-catalog.js +364 -0
  125. package/dist/lib/server-bootstrap.js +240 -0
  126. package/dist/lib/setup-agent-assets-bridge.js +176 -0
  127. package/dist/lib/setup-agent-assets-release.js +52 -0
  128. package/dist/lib/setup-external-postgres.js +108 -0
  129. package/dist/lib/setup-init-orchestrator.js +990 -0
  130. package/dist/lib/setup-install-guide.js +52 -0
  131. package/dist/lib/setup-installer-release.js +47 -0
  132. package/dist/lib/setup-local-postgres.js +329 -0
  133. package/dist/lib/setup-official-catalog-bridge.js +148 -0
  134. package/dist/lib/setup-readiness.js +888 -0
  135. package/dist/lib/tui-launcher.js +59 -0
  136. package/dist/main.js +150 -0
  137. package/dist/seeds/agents-md.js +54 -0
  138. package/dist/utils/agent-gateway.js +275 -0
  139. package/dist/utils/agents-md.js +281 -0
  140. package/dist/utils/api.js +255 -0
  141. package/dist/utils/chatv3-session-store.js +185 -0
  142. package/dist/utils/command.js +67 -0
  143. package/dist/utils/community-host-runtime.js +376 -0
  144. package/dist/utils/config.js +1015 -0
  145. package/dist/utils/discussion-workspace.js +552 -0
  146. package/dist/utils/experience-workspace.js +326 -0
  147. package/dist/utils/guide-paths.js +67 -0
  148. package/dist/utils/hosted-sugar.js +56 -0
  149. package/dist/utils/hosted-workspace.js +229 -0
  150. package/dist/utils/memory-workspace.js +537 -0
  151. package/dist/utils/operator-runtime.js +199 -0
  152. package/dist/utils/password-crypto.js +39 -0
  153. package/dist/utils/playbook-workspace.js +479 -0
  154. package/dist/utils/project-context.js +255 -0
  155. package/dist/utils/prompts.js +70 -0
  156. package/dist/utils/refresh-race.js +28 -0
  157. package/dist/utils/repo-config.js +109 -0
  158. package/dist/utils/repo-first-projectman.js +194 -0
  159. package/dist/utils/repo-first-storage.js +13 -0
  160. package/dist/utils/repo-first-view.js +305 -0
  161. package/dist/utils/session-guidance.js +278 -0
  162. package/dist/utils/session-state.js +137 -0
  163. package/dist/utils/table.js +39 -0
  164. package/dist/utils/transient-fs.js +49 -0
  165. package/dist/utils/workspace-root.js +1 -0
  166. package/launchers/aops-cockpit.ps1 +15 -0
  167. package/launchers/aops-cockpit.sh +16 -0
  168. package/launchers/aops-server.ps1 +15 -0
  169. package/launchers/aops-server.sh +16 -0
  170. package/native/tui/win32-x64/aops-tui.exe +0 -0
  171. package/package.json +81 -0
@@ -0,0 +1,504 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFile, spawn } from 'node:child_process';
3
+ import { lstatSync, readFileSync, readdirSync, realpathSync, } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { compareCanonicalText, CommercialArtifactError, commercialRoleLaunchSpecBinding, setupReadinessReceiptBinding, verifySetupReadinessFinalReadback, } from '@aopslabs/artifact-trust-contracts';
6
+ function execListenerTool(file, args) {
7
+ return new Promise((resolve, reject) => {
8
+ execFile(file, [...args], {
9
+ windowsHide: true,
10
+ maxBuffer: 8 * 1024 * 1024,
11
+ encoding: 'utf8',
12
+ env: Object.fromEntries(['SystemRoot', 'WINDIR', 'PATH', 'LANG', 'LC_ALL']
13
+ .flatMap((key) => process.env[key] ? [[key, process.env[key]]] : [])),
14
+ }, (error, stdout) => error ? reject(error) : resolve(stdout));
15
+ });
16
+ }
17
+ export const inspectSystemCommercialListeners = async (processes) => {
18
+ const byPid = new Map([
19
+ [processes.server, []],
20
+ [processes.cockpit, []],
21
+ ]);
22
+ try {
23
+ if (process.platform === 'win32') {
24
+ const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? 'C:\\Windows';
25
+ const output = await execListenerTool(path.join(systemRoot, 'System32', 'netstat.exe'), ['-ano', '-p', 'tcp']);
26
+ for (const line of output.split(/\r?\n/)) {
27
+ const match = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
28
+ if (!match)
29
+ continue;
30
+ const addresses = byPid.get(Number(match[2]));
31
+ if (addresses)
32
+ addresses.push(match[1]);
33
+ }
34
+ }
35
+ else if (process.platform === 'linux') {
36
+ const output = await execListenerTool('ss', ['-ltnpH']);
37
+ for (const line of output.split(/\r?\n/)) {
38
+ const pidMatch = /pid=(\d+)/.exec(line);
39
+ if (!pidMatch)
40
+ continue;
41
+ const addresses = byPid.get(Number(pidMatch[1]));
42
+ if (!addresses)
43
+ continue;
44
+ const columns = line.trim().split(/\s+/);
45
+ if (columns[3])
46
+ addresses.push(columns[3]);
47
+ }
48
+ }
49
+ else if (process.platform === 'darwin') {
50
+ const output = await execListenerTool('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN']);
51
+ for (const line of output.split(/\r?\n/).slice(1)) {
52
+ const columns = line.trim().split(/\s+/);
53
+ const addresses = byPid.get(Number(columns[1]));
54
+ const endpoint = columns.at(-2);
55
+ if (addresses && endpoint)
56
+ addresses.push(endpoint.replace(/^TCP\s*/, ''));
57
+ }
58
+ }
59
+ else {
60
+ fail('final_listener_inspection_unsupported');
61
+ }
62
+ }
63
+ catch {
64
+ throw new Error('commercial_listener_inspection_failed');
65
+ }
66
+ return Object.freeze({
67
+ server: Object.freeze({ pid: processes.server, addresses: Object.freeze(byPid.get(processes.server) ?? []) }),
68
+ cockpit: Object.freeze({ pid: processes.cockpit, addresses: Object.freeze(byPid.get(processes.cockpit) ?? []) }),
69
+ });
70
+ };
71
+ const SECRET_ENV_KEY = 'AOPS_PG_URL';
72
+ const SECRET_PATTERN = /postgres(?:ql)?:\/\/|password|passwd|token|secret/i;
73
+ function fail(reason, decision = 'deny') {
74
+ throw new CommercialArtifactError('commercial-final-readiness', reason, decision);
75
+ }
76
+ function containsSecretValue(value) {
77
+ if (typeof value === 'string')
78
+ return SECRET_PATTERN.test(value);
79
+ if (Array.isArray(value))
80
+ return value.some(containsSecretValue);
81
+ if (value && typeof value === 'object')
82
+ return Object.values(value).some(containsSecretValue);
83
+ return false;
84
+ }
85
+ function within(root, candidate) {
86
+ const relative = path.relative(root, candidate);
87
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
88
+ }
89
+ function hashStoreEntry(root, candidate, hash) {
90
+ const stat = lstatSync(candidate);
91
+ if (stat.isSymbolicLink())
92
+ fail('final_store_symlink_refused', 'quarantine');
93
+ const real = realpathSync.native(candidate);
94
+ if (!within(root, real))
95
+ fail('final_store_path_escape', 'quarantine');
96
+ const relative = path.relative(root, candidate).split(path.sep).join('/') || '.';
97
+ if (stat.isDirectory()) {
98
+ hash.update(`d\0${relative}\0`);
99
+ for (const name of readdirSync(candidate).sort(compareCanonicalText)) {
100
+ hashStoreEntry(root, path.join(candidate, name), hash);
101
+ }
102
+ return;
103
+ }
104
+ if (!stat.isFile())
105
+ fail('final_store_entry_unsafe', 'quarantine');
106
+ const bytes = readFileSync(candidate);
107
+ hash.update(`f\0${relative}\0${bytes.length}\0`);
108
+ hash.update(bytes);
109
+ }
110
+ export function digestCommercialStoreTree(storeRoot) {
111
+ const resolved = path.resolve(storeRoot);
112
+ const stat = lstatSync(resolved);
113
+ if (!stat.isDirectory() || stat.isSymbolicLink())
114
+ fail('final_store_root_unsafe', 'quarantine');
115
+ const canonical = realpathSync.native(resolved);
116
+ if (canonical !== resolved)
117
+ fail('final_store_root_alias_refused', 'quarantine');
118
+ const hash = createHash('sha256');
119
+ hashStoreEntry(canonical, canonical, hash);
120
+ return `sha256:${hash.digest('hex')}`;
121
+ }
122
+ function defaultSpawnAuthority(spec, privateEnvironment) {
123
+ const child = spawn(spec.command, [...spec.args], {
124
+ cwd: spec.cwd,
125
+ shell: spec.shell,
126
+ windowsHide: spec.windowsHide,
127
+ env: { ...spec.env, ...privateEnvironment },
128
+ stdio: ['ignore', 'inherit', 'inherit'],
129
+ });
130
+ const exited = new Promise((resolve) => {
131
+ child.once('exit', (code, signal) => resolve(Object.freeze({ code, signal })));
132
+ child.once('error', () => resolve(Object.freeze({ code: null, signal: null })));
133
+ });
134
+ if (typeof child.pid !== 'number')
135
+ fail('final_service_spawn_failed');
136
+ return Object.freeze({
137
+ pid: child.pid,
138
+ exited,
139
+ terminate: () => { child.kill('SIGTERM'); },
140
+ kill: () => { child.kill('SIGKILL'); },
141
+ });
142
+ }
143
+ function assertSpecSafe(spec, expectedRole) {
144
+ const verified = commercialRoleLaunchSpecBinding(spec);
145
+ if (verified.role !== expectedRole)
146
+ fail('final_launch_role_mismatch');
147
+ if (verified.shell !== false || !path.isAbsolute(verified.command) || !path.isAbsolute(verified.args[0] ?? '') ||
148
+ Object.values(verified.env).some((value) => SECRET_PATTERN.test(value)) ||
149
+ verified.args.some((value) => SECRET_PATTERN.test(value)) ||
150
+ verified.args.some((value) => /(?:^|[^0-9])0\.0\.0\.0(?:[^0-9]|$)/.test(value)))
151
+ fail('final_launch_spec_unsafe', 'quarantine');
152
+ return verified;
153
+ }
154
+ async function waitForExit(child, timeoutMs) {
155
+ let timer;
156
+ const timeout = new Promise((resolve) => {
157
+ timer = setTimeout(() => resolve(false), timeoutMs);
158
+ });
159
+ const exited = child.exited.then(() => true);
160
+ const result = await Promise.race([exited, timeout]);
161
+ if (timer)
162
+ clearTimeout(timer);
163
+ return result;
164
+ }
165
+ export function createCommercialServiceSupervisor(options) {
166
+ const specs = {
167
+ server: assertSpecSafe(options.serverSpec, 'server'),
168
+ cockpit: assertSpecSafe(options.cockpitSpec, 'cockpit'),
169
+ };
170
+ const spawnAuthority = options.spawnAuthority ?? defaultSpawnAuthority;
171
+ const maxRestarts = Math.min(Math.max(options.maxRestartAttempts ?? 2, 0), 10);
172
+ const restartBackoffMs = Math.min(Math.max(options.restartBackoffMs ?? 250, 0), 30_000);
173
+ const gracefulStopMs = Math.min(Math.max(options.gracefulStopMs ?? 5_000, 100), 30_000);
174
+ const states = {
175
+ server: { child: null, state: 'stopped', generation: 0, restartCount: 0, intentionalStop: false, failureCode: null, restartTimer: null },
176
+ cockpit: { child: null, state: 'stopped', generation: 0, restartCount: 0, intentionalStop: false, failureCode: null, restartTimer: null },
177
+ };
178
+ let shuttingDown = false;
179
+ let exitHookRegistered = false;
180
+ const killChildrenOnParentExit = () => {
181
+ for (const role of ['cockpit', 'server'])
182
+ states[role].child?.kill();
183
+ };
184
+ const spawnRole = async (role) => {
185
+ const state = states[role];
186
+ if (shuttingDown)
187
+ throw new Error('commercial_services_shutting_down');
188
+ let privateEnvironment = Object.freeze({});
189
+ if (role === 'server') {
190
+ let value = '';
191
+ try {
192
+ value = await options.readServerPostgresUrl();
193
+ }
194
+ catch {
195
+ throw new Error('commercial_service_postgres_credential_unavailable');
196
+ }
197
+ if (!value || /[\u0000\r\n]/.test(value)) {
198
+ throw new Error('commercial_service_postgres_credential_unavailable');
199
+ }
200
+ privateEnvironment = Object.freeze({ [SECRET_ENV_KEY]: value });
201
+ }
202
+ const child = spawnAuthority(specs[role], privateEnvironment);
203
+ state.child = child;
204
+ state.state = 'running';
205
+ state.intentionalStop = false;
206
+ state.failureCode = null;
207
+ state.generation += 1;
208
+ void child.exited.then(() => {
209
+ if (state.child !== child)
210
+ return;
211
+ state.child = null;
212
+ if (state.intentionalStop || shuttingDown) {
213
+ state.state = 'stopped';
214
+ return;
215
+ }
216
+ if (state.restartCount >= maxRestarts) {
217
+ state.state = 'failed';
218
+ state.failureCode = `commercial_${role}_restart_limit_reached`;
219
+ return;
220
+ }
221
+ state.restartCount += 1;
222
+ state.state = 'restart-wait';
223
+ state.restartTimer = setTimeout(() => {
224
+ state.restartTimer = null;
225
+ void spawnRole(role).catch(() => {
226
+ state.state = 'failed';
227
+ state.failureCode = `commercial_${role}_restart_failed`;
228
+ });
229
+ }, restartBackoffMs * state.restartCount);
230
+ });
231
+ };
232
+ const stopRole = async (role) => {
233
+ const state = states[role];
234
+ state.intentionalStop = true;
235
+ if (state.restartTimer) {
236
+ clearTimeout(state.restartTimer);
237
+ state.restartTimer = null;
238
+ }
239
+ const child = state.child;
240
+ if (!child) {
241
+ state.state = 'stopped';
242
+ return;
243
+ }
244
+ child.terminate();
245
+ if (!await waitForExit(child, gracefulStopMs)) {
246
+ child.kill();
247
+ if (!await waitForExit(child, gracefulStopMs)) {
248
+ throw new Error(`commercial_${role}_child_cleanup_timeout`);
249
+ }
250
+ }
251
+ if (state.child === child)
252
+ state.child = null;
253
+ state.state = 'stopped';
254
+ };
255
+ return Object.freeze({
256
+ start: async () => {
257
+ if (!exitHookRegistered) {
258
+ process.once('exit', killChildrenOnParentExit);
259
+ exitHookRegistered = true;
260
+ }
261
+ await spawnRole('server');
262
+ try {
263
+ await spawnRole('cockpit');
264
+ }
265
+ catch (error) {
266
+ await stopRole('server').catch(() => undefined);
267
+ process.removeListener('exit', killChildrenOnParentExit);
268
+ exitHookRegistered = false;
269
+ throw error;
270
+ }
271
+ },
272
+ stopRole,
273
+ restartRole: async (role, nextSpec) => {
274
+ const replacement = nextSpec === undefined ? null : assertSpecSafe(nextSpec, role);
275
+ await stopRole(role);
276
+ if (replacement)
277
+ specs[role] = replacement;
278
+ states[role].restartCount = 0;
279
+ await spawnRole(role);
280
+ },
281
+ shutdown: async () => {
282
+ shuttingDown = true;
283
+ const results = await Promise.allSettled([stopRole('cockpit'), stopRole('server')]);
284
+ if (exitHookRegistered) {
285
+ process.removeListener('exit', killChildrenOnParentExit);
286
+ exitHookRegistered = false;
287
+ }
288
+ if (results.some((result) => result.status === 'rejected')) {
289
+ throw new Error('commercial_service_shutdown_incomplete');
290
+ }
291
+ },
292
+ status: () => Object.freeze({
293
+ server: Object.freeze({
294
+ state: states.server.state,
295
+ pid: states.server.child?.pid ?? null,
296
+ generation: states.server.generation,
297
+ restartCount: states.server.restartCount,
298
+ failureCode: states.server.failureCode,
299
+ }),
300
+ cockpit: Object.freeze({
301
+ state: states.cockpit.state,
302
+ pid: states.cockpit.child?.pid ?? null,
303
+ generation: states.cockpit.generation,
304
+ restartCount: states.cockpit.restartCount,
305
+ failureCode: states.cockpit.failureCode,
306
+ }),
307
+ }),
308
+ });
309
+ }
310
+ function loopbackEndpoint(value) {
311
+ let url;
312
+ try {
313
+ url = new URL(value);
314
+ }
315
+ catch {
316
+ return fail('final_health_endpoint_invalid');
317
+ }
318
+ if (url.protocol !== 'http:' || !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) ||
319
+ url.username || url.password || url.search || url.hash)
320
+ fail('final_health_endpoint_not_loopback');
321
+ return url;
322
+ }
323
+ async function probe(url, fetchFn) {
324
+ try {
325
+ const response = await fetchFn(url, { redirect: 'error', cache: 'no-store' });
326
+ const bytes = Buffer.from(await response.arrayBuffer());
327
+ return Object.freeze({
328
+ ok: response.ok,
329
+ body: bytes.toString('utf8'),
330
+ bytes,
331
+ contentType: response.headers.get('content-type') ?? '',
332
+ });
333
+ }
334
+ catch {
335
+ return Object.freeze({ ok: false, body: '', bytes: Buffer.alloc(0), contentType: '' });
336
+ }
337
+ }
338
+ async function waitForProbe(url, expected, fetchFn, timeoutMs, axis, marker) {
339
+ const deadline = Date.now() + timeoutMs;
340
+ for (;;) {
341
+ const result = await probe(url, fetchFn);
342
+ const matched = expected === 'up'
343
+ ? result.ok && (marker === undefined || result.body.includes(marker))
344
+ : !result.ok;
345
+ if (matched)
346
+ return result;
347
+ if (Date.now() >= deadline)
348
+ fail(`final_health_${axis}_${expected}_timeout`);
349
+ await new Promise((resolve) => setTimeout(resolve, 100));
350
+ }
351
+ }
352
+ async function waitForStaticProbe(url, fetchFn, timeoutMs, expectedSha256, expectedContentType) {
353
+ if (!/^sha256:[a-f0-9]{64}$/.test(expectedSha256))
354
+ fail('final_health_cockpit_static_digest_invalid');
355
+ const deadline = Date.now() + timeoutMs;
356
+ for (;;) {
357
+ const result = await probe(url, fetchFn);
358
+ const mediaType = result.contentType.split(';', 1)[0]?.trim().toLowerCase();
359
+ const digest = `sha256:${createHash('sha256').update(result.bytes).digest('hex')}`;
360
+ if (result.ok && result.bytes.length > 0 && mediaType === expectedContentType && digest === expectedSha256)
361
+ return;
362
+ if (Date.now() >= deadline)
363
+ fail('final_health_cockpit-static_up_timeout');
364
+ await new Promise((resolve) => setTimeout(resolve, 100));
365
+ }
366
+ }
367
+ async function assertLoopbackListeners(supervisor, inspectListeners) {
368
+ const status = supervisor.status();
369
+ if (status.server.pid === null || status.cockpit.pid === null)
370
+ fail('final_service_process_missing');
371
+ const expected = Object.freeze({ server: status.server.pid, cockpit: status.cockpit.pid });
372
+ const observed = await inspectListeners(expected);
373
+ for (const role of ['server', 'cockpit']) {
374
+ const entry = observed[role];
375
+ if (!entry || entry.pid !== expected[role] || !Array.isArray(entry.addresses) || entry.addresses.length === 0) {
376
+ fail(`final_${role}_listener_readback_invalid`, 'quarantine');
377
+ }
378
+ if (entry.addresses.some((address) => !/^(?:127(?:\.[0-9]{1,3}){3}|localhost|\[::1\]):[1-9][0-9]{0,4}$/.test(address))) {
379
+ fail(`final_${role}_listener_not_loopback`, 'quarantine');
380
+ }
381
+ }
382
+ }
383
+ export async function verifyCommercialServiceHealthAndRecovery(options) {
384
+ const fetchFn = options.fetchFn ?? fetch;
385
+ const timeoutMs = Math.min(Math.max(options.timeoutMs ?? 30_000, 1_000), 120_000);
386
+ const urls = Object.freeze({
387
+ server: loopbackEndpoint(options.endpoints.serverDirect),
388
+ root: loopbackEndpoint(options.endpoints.cockpitRoot),
389
+ static: loopbackEndpoint(options.endpoints.cockpitStatic),
390
+ missingStatic: loopbackEndpoint(options.endpoints.cockpitMissingStatic),
391
+ proxy: loopbackEndpoint(options.endpoints.cockpitProxy),
392
+ });
393
+ if (urls.root.origin !== urls.static.origin || urls.root.origin !== urls.missingStatic.origin || urls.root.origin !== urls.proxy.origin ||
394
+ urls.server.origin === urls.root.origin || urls.root.pathname === urls.static.pathname ||
395
+ urls.root.pathname === urls.missingStatic.pathname || urls.root.pathname === urls.proxy.pathname ||
396
+ urls.static.pathname === urls.missingStatic.pathname || urls.static.pathname === urls.proxy.pathname ||
397
+ urls.missingStatic.pathname === urls.proxy.pathname)
398
+ fail('final_health_axes_not_distinct');
399
+ await waitForProbe(urls.server, 'up', fetchFn, timeoutMs, 'server', options.endpoints.serverResponseMarker);
400
+ await waitForProbe(urls.root, 'up', fetchFn, timeoutMs, 'cockpit-root');
401
+ await waitForStaticProbe(urls.static, fetchFn, timeoutMs, options.endpoints.cockpitStaticSha256, options.endpoints.cockpitStaticContentType);
402
+ await waitForProbe(urls.missingStatic, 'down', fetchFn, timeoutMs, 'cockpit-static-missing');
403
+ await waitForProbe(urls.proxy, 'up', fetchFn, timeoutMs, 'cockpit-proxy', options.endpoints.serverResponseMarker);
404
+ await assertLoopbackListeners(options.supervisor, options.inspectListeners);
405
+ const cockpitBefore = options.supervisor.status().cockpit;
406
+ await options.supervisor.stopRole('server');
407
+ await waitForProbe(urls.proxy, 'down', fetchFn, timeoutMs, 'cockpit-proxy');
408
+ if (options.supervisor.status().cockpit.pid !== cockpitBefore.pid) {
409
+ fail('final_cockpit_not_independent', 'quarantine');
410
+ }
411
+ await options.supervisor.restartRole('server');
412
+ await waitForProbe(urls.server, 'up', fetchFn, timeoutMs, 'server', options.endpoints.serverResponseMarker);
413
+ await waitForProbe(urls.proxy, 'up', fetchFn, timeoutMs, 'cockpit-proxy', options.endpoints.serverResponseMarker);
414
+ await assertLoopbackListeners(options.supervisor, options.inspectListeners);
415
+ if (options.supervisor.status().cockpit.pid !== cockpitBefore.pid) {
416
+ fail('final_cockpit_restarted_during_server_recovery', 'quarantine');
417
+ }
418
+ const serverBeforeCockpitRecovery = options.supervisor.status().server;
419
+ await options.supervisor.stopRole('cockpit');
420
+ await waitForProbe(urls.root, 'down', fetchFn, timeoutMs, 'cockpit-root');
421
+ await waitForProbe(urls.static, 'down', fetchFn, timeoutMs, 'cockpit-static');
422
+ await waitForProbe(urls.proxy, 'down', fetchFn, timeoutMs, 'cockpit-proxy');
423
+ await waitForProbe(urls.server, 'up', fetchFn, timeoutMs, 'server', options.endpoints.serverResponseMarker);
424
+ if (options.supervisor.status().server.pid !== serverBeforeCockpitRecovery.pid) {
425
+ fail('final_server_not_independent', 'quarantine');
426
+ }
427
+ await options.supervisor.restartRole('cockpit');
428
+ await waitForProbe(urls.root, 'up', fetchFn, timeoutMs, 'cockpit-root');
429
+ await waitForStaticProbe(urls.static, fetchFn, timeoutMs, options.endpoints.cockpitStaticSha256, options.endpoints.cockpitStaticContentType);
430
+ await waitForProbe(urls.missingStatic, 'down', fetchFn, timeoutMs, 'cockpit-static-missing');
431
+ await waitForProbe(urls.proxy, 'up', fetchFn, timeoutMs, 'cockpit-proxy', options.endpoints.serverResponseMarker);
432
+ await assertLoopbackListeners(options.supervisor, options.inspectListeners);
433
+ if (options.supervisor.status().server.pid !== serverBeforeCockpitRecovery.pid) {
434
+ fail('final_server_restarted_during_cockpit_recovery', 'quarantine');
435
+ }
436
+ }
437
+ export async function runCommercialFinalReadiness(options) {
438
+ const serverSpec = assertSpecSafe(options.serverSpec, 'server');
439
+ const cockpitSpec = assertSpecSafe(options.cockpitSpec, 'cockpit');
440
+ const readinessReceipt = setupReadinessReceiptBinding(options.readinessReceipt);
441
+ if (serverSpec.readback.receiptId !== cockpitSpec.readback.receiptId) {
442
+ fail('final_launch_release_mismatch', 'quarantine');
443
+ }
444
+ if (serverSpec.readback.receiptId !== readinessReceipt.releaseReceiptId ||
445
+ JSON.stringify(serverSpec.readback) !== JSON.stringify(cockpitSpec.readback))
446
+ fail('final_readiness_release_mismatch', 'quarantine');
447
+ const before = digestCommercialStoreTree(options.storeRoot);
448
+ const supervisor = createCommercialServiceSupervisor({
449
+ serverSpec,
450
+ cockpitSpec,
451
+ readServerPostgresUrl: options.readServerPostgresUrl,
452
+ spawnAuthority: options.spawnAuthority,
453
+ });
454
+ try {
455
+ await supervisor.start();
456
+ await verifyCommercialServiceHealthAndRecovery({
457
+ supervisor,
458
+ endpoints: options.health,
459
+ inspectListeners: options.inspectListeners ?? inspectSystemCommercialListeners,
460
+ fetchFn: options.fetchFn,
461
+ timeoutMs: options.timeoutMs,
462
+ });
463
+ await verifySetupReadinessFinalReadback({
464
+ plan: options.readinessPlan,
465
+ receipt: readinessReceipt,
466
+ postgresReceipt: options.postgresReceipt,
467
+ adapter: options.readinessAdapter,
468
+ });
469
+ const after = digestCommercialStoreTree(options.storeRoot);
470
+ if (after !== before)
471
+ fail('final_store_digest_changed', 'quarantine');
472
+ const receipt = Object.freeze({
473
+ schemaVersion: 1,
474
+ objectType: 'aops-commercial-final-readiness-receipt-v1',
475
+ readinessPlanId: readinessReceipt.planId,
476
+ releaseReceiptId: readinessReceipt.releaseReceiptId,
477
+ serverLaunchReceiptId: serverSpec.readback.receiptId,
478
+ cockpitLaunchReceiptId: cockpitSpec.readback.receiptId,
479
+ storeDigest: after,
480
+ readinessReadbackDigest: readinessReceipt.readbackDigest,
481
+ verification: Object.freeze({
482
+ launchSpecs: 'verified-verbatim',
483
+ independentSupervision: 'verified',
484
+ directServerHealth: 'verified',
485
+ cockpitRoot: 'verified',
486
+ cockpitStatic: 'verified',
487
+ cockpitProxyRecovery: 'verified',
488
+ cockpitIndependentRecovery: 'verified',
489
+ loopbackOnly: 'verified',
490
+ finalLiveReadback: 'verified',
491
+ immutableStore: 'verified',
492
+ secretsSerialized: false,
493
+ }),
494
+ });
495
+ if (containsSecretValue(receipt)) {
496
+ fail('final_receipt_secret_detected', 'quarantine');
497
+ }
498
+ return Object.freeze({ receipt, supervisor });
499
+ }
500
+ catch (error) {
501
+ await supervisor.shutdown().catch(() => undefined);
502
+ throw error;
503
+ }
504
+ }