@empire-builder-kit/nx 1.0.0-rc.40 → 1.0.0-rc.42

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 (58) hide show
  1. package/README.md +26 -10
  2. package/package.json +3 -3
  3. package/src/blueprints/external-sandbox.d.ts +1 -1
  4. package/src/cli/deploy-runtime.d.ts +25 -1
  5. package/src/cli/deploy-runtime.js +120 -4
  6. package/src/cli/dev-command.d.ts +2 -0
  7. package/src/cli/dev-command.js +8 -0
  8. package/src/cli/main.js +20 -6
  9. package/src/cli/personal-secret-runtime.d.ts +12 -0
  10. package/src/cli/personal-secret-runtime.js +18 -3
  11. package/src/cli/secret-terminal.js +1 -1
  12. package/src/container-images.d.ts +1 -1
  13. package/src/container-images.js +1 -1
  14. package/src/developer-agent/operation-registry.d.ts +10 -0
  15. package/src/developer-agent/operation-registry.js +60 -13
  16. package/src/developer-agent/planning.d.ts +8 -1
  17. package/src/developer-agent/planning.js +23 -3
  18. package/src/executors/local-deploy/local-approval.js +1 -1
  19. package/src/executors/sst-lifecycle/governed-native-operation.js +3 -2
  20. package/src/executors/sst-lifecycle/run-sst-command.d.ts +6 -0
  21. package/src/executors/sst-lifecycle/run-sst-command.js +3 -33
  22. package/src/foundation/cost-estimate.d.ts +170 -0
  23. package/src/foundation/cost-estimate.js +683 -0
  24. package/src/generators/preset/tool-versions.d.ts +0 -2
  25. package/src/generators/preset/tool-versions.js +1 -3
  26. package/src/generators/preset/workspace-files-adopter-skills.js +39 -18
  27. package/src/generators/preset/workspace-files-developer-agent.js +83 -27
  28. package/src/generators/preset/workspace-files-shared-operation-claims.d.ts +10 -0
  29. package/src/generators/preset/workspace-files-shared-operation-claims.js +59 -0
  30. package/src/generators/preset/workspace-files-stage-authority.js +13 -2
  31. package/src/generators/preset/workspace-files-thin-deploy.d.ts +10 -0
  32. package/src/generators/preset/workspace-files-thin-deploy.js +54 -20
  33. package/src/generators/preset/workspace-files-thin-docs.js +32 -14
  34. package/src/generators/preset/workspace-files.js +28 -8
  35. package/src/generators/slice/built-in-plan-api-contract-files.js +7 -4
  36. package/src/generators/slice/built-in-plan-authorization-files.js +10 -5
  37. package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +933 -249
  38. package/src/generators/slice/built-in-plan-data-files.js +17 -8
  39. package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js +5 -1
  40. package/src/generators/slice/built-in-plan-frontend-files.js +7 -0
  41. package/src/generators/slice/built-in-plan-infrastructure-files.js +0 -1
  42. package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +8 -3
  43. package/src/generators/slice/built-in-plan-metadata-files.js +2 -1
  44. package/src/generators/slice/built-in-plan-reliability-files.js +4 -1
  45. package/src/generators/slice/built-in-plan-shared.d.ts +2 -2
  46. package/src/generators/slice/built-in-plan-shared.js +2 -2
  47. package/src/generators/slice/built-in-plan-testing-persistence-event-files.d.ts +7 -0
  48. package/src/generators/slice/built-in-plan-testing-persistence-event-files.js +487 -0
  49. package/src/generators/slice/built-in-plan-testing-persistence-files.js +2 -0
  50. package/src/generators/slice/built-in-plan.js +3 -1
  51. package/src/generators/workload/frozen-container.d.ts +1 -1
  52. package/src/git-policy/github-bootstrap.d.ts +9 -0
  53. package/src/git-policy/github-bootstrap.js +16 -2
  54. package/src/local-dev/fleet-inventory.js +18 -10
  55. package/src/local-dev/fleet-launcher.js +2 -2
  56. package/src/secrets/personal-command.d.ts +18 -2
  57. package/src/secrets/personal-command.js +66 -12
  58. package/src/secrets/verification.js +6 -1
@@ -22,6 +22,14 @@ function inventoryError(message) {
22
22
  function defaultPing(appData, security, fleetId) {
23
23
  return new fleet_client_js_1.FleetClient((0, runtime_paths_js_1.fleetRuntimePaths)(fleetId, appData), security).ping();
24
24
  }
25
+ /**
26
+ * An ambiguous entry is never pruned, but any actor may inspect it read-only.
27
+ * The message names where it lives and the owner's recovery; it never prints
28
+ * an absolute path, which would carry the local account name.
29
+ */
30
+ function inspectReadOnly(subject, location) {
31
+ return `${subject}; ebk will not prune it. Inspect ${location} in the per-user EBK application-data directory read-only (any actor may). If it is yours and no ebk dev process for it is running, remove that one entry or restore current-user-only ownership, then rerun ebk dev ps; if another user owns it, leave it for them.`;
32
+ }
25
33
  function record(fleetId, classification, nextAction, state) {
26
34
  return {
27
35
  ...(state
@@ -51,7 +59,7 @@ async function inspectFleet(fleetId, options, effects, recoverRunnerCleanup = fa
51
59
  }
52
60
  catch {
53
61
  return {
54
- inventory: record(fleetId, 'ambiguous', 'Inspect the malformed private state manually; do not prune.'),
62
+ inventory: record(fleetId, 'ambiguous', inspectReadOnly('Private Fleet state is malformed', `sessions/${fleetId}`)),
55
63
  };
56
64
  }
57
65
  if (!state || state.fleetId !== fleetId) {
@@ -197,7 +205,7 @@ async function inventoryFleets(options, effects = {}) {
197
205
  results.push({
198
206
  classification: 'ambiguous',
199
207
  fleetId: entry.name.slice(0, 128),
200
- nextAction: 'Unsupported session entry; inspect manually and do not prune.',
208
+ nextAction: inspectReadOnly('Unsupported session entry', 'sessions/'),
201
209
  selectedSlices: [],
202
210
  });
203
211
  continue;
@@ -259,7 +267,7 @@ async function inventorySliceClaims(options, effects = {}) {
259
267
  }
260
268
  catch {
261
269
  return [
262
- ambiguousClaimEntry('slice-claims', 'Slice-claim directory is not current-user private; inspect manually and do not prune.'),
270
+ ambiguousClaimEntry('slice-claims', inspectReadOnly('Slice-claim directory is not current-user private', 'slice-claims/')),
263
271
  ];
264
272
  }
265
273
  let entries;
@@ -268,7 +276,7 @@ async function inventorySliceClaims(options, effects = {}) {
268
276
  }
269
277
  catch {
270
278
  return [
271
- ambiguousClaimEntry('slice-claims', 'Slice-claim directory cannot be read safely; inspect manually and do not prune.'),
279
+ ambiguousClaimEntry('slice-claims', inspectReadOnly('Slice-claim directory cannot be read safely', 'slice-claims/')),
272
280
  ];
273
281
  }
274
282
  const inspectedFleets = new Map();
@@ -286,7 +294,7 @@ async function inventorySliceClaims(options, effects = {}) {
286
294
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(namespace.name) ||
287
295
  !namespace.isDirectory() ||
288
296
  namespace.isSymbolicLink()) {
289
- results.push(ambiguousClaimEntry(namespace.name, 'Unsupported Slice-claim namespace; inspect manually and do not prune.', /^[a-z0-9][a-z0-9-]{0,62}$/.test(namespace.name)
297
+ results.push(ambiguousClaimEntry(namespace.name, inspectReadOnly('Unsupported Slice-claim namespace', 'slice-claims/'), /^[a-z0-9][a-z0-9-]{0,62}$/.test(namespace.name)
290
298
  ? namespace.name
291
299
  : undefined));
292
300
  continue;
@@ -304,13 +312,13 @@ async function inventorySliceClaims(options, effects = {}) {
304
312
  });
305
313
  }
306
314
  catch {
307
- results.push(ambiguousClaimEntry(namespace.name, 'Slice-claim namespace cannot be read safely; inspect manually and do not prune.', namespace.name));
315
+ results.push(ambiguousClaimEntry(namespace.name, inspectReadOnly('Slice-claim namespace cannot be read safely', 'slice-claims/'), namespace.name));
308
316
  }
309
317
  }
310
318
  for (const { entry, directory: claimDirectory, fleetId } of claims) {
311
319
  const match = SLICE_CLAIM_FILE_PATTERN.exec(entry.name);
312
320
  if (!match || !entry.isFile() || entry.isSymbolicLink()) {
313
- results.push(ambiguousClaimEntry(entry.name, 'Unsupported Slice-claim entry; inspect manually and do not prune.', fleetId));
321
+ results.push(ambiguousClaimEntry(entry.name, inspectReadOnly('Unsupported Slice-claim entry', 'slice-claims/'), fleetId));
314
322
  continue;
315
323
  }
316
324
  const slice = match[1];
@@ -318,17 +326,17 @@ async function inventorySliceClaims(options, effects = {}) {
318
326
  try {
319
327
  const inspected = (0, slice_claim_js_1.inspectSliceClaim)(claimDirectory, slice, security);
320
328
  if (!inspected) {
321
- results.push(ambiguousClaimEntry(slice, 'Slice claim changed during inventory; inspect manually and do not prune.', fleetId));
329
+ results.push(ambiguousClaimEntry(slice, 'Slice claim changed during inventory; ebk will not prune it. Rerun ebk dev ps; if the change persists, inspect slice-claims/ in the per-user EBK application-data directory read-only.', fleetId));
322
330
  continue;
323
331
  }
324
332
  if (inspected.slice !== slice || inspected.fleetId !== fleetId) {
325
- results.push(ambiguousClaimEntry(slice, 'Slice-claim namespace, filename and record identity disagree; inspect manually and do not prune.', fleetId));
333
+ results.push(ambiguousClaimEntry(slice, inspectReadOnly('Slice-claim namespace, filename and record identity disagree', 'slice-claims/'), fleetId));
326
334
  continue;
327
335
  }
328
336
  claim = inspected;
329
337
  }
330
338
  catch {
331
- results.push(ambiguousClaimEntry(slice, 'Slice claim is malformed or not current-user private; inspect manually and do not prune.', fleetId));
339
+ results.push(ambiguousClaimEntry(slice, inspectReadOnly('Slice claim is malformed or not current-user private', 'slice-claims/'), fleetId));
332
340
  continue;
333
341
  }
334
342
  const ownerPaths = (0, runtime_paths_js_1.fleetRuntimePaths)(claim.fleetId, options.appData);
@@ -230,7 +230,7 @@ function recoverStartupOnlyRequest(prepared, processProbe, security) {
230
230
  startup = (0, fleet_startup_js_1.readFleetStartupRequest)(prepared.paths.startup, security);
231
231
  }
232
232
  catch {
233
- return launchError('stale-state-ambiguous', 'Residual Fleet startup authority is malformed; inspect it manually and do not replace it.');
233
+ return launchError('stale-state-ambiguous', `Residual Fleet startup authority is malformed; ebk will not replace it. Inspect sessions/${prepared.request.fleetId} in the per-user EBK application-data directory read-only (any actor may). If it is yours and no ebk dev process for it is running, remove that startup file, then rerun the command.`);
234
234
  }
235
235
  if (startup.fleetId !== prepared.request.fleetId ||
236
236
  startup.appData !== prepared.request.appData ||
@@ -270,7 +270,7 @@ function recoverStartupOnlyRequest(prepared, processProbe, security) {
270
270
  sessionGeneration: startup.sessionGeneration,
271
271
  }).slice(0, 16)}.recover`;
272
272
  if ((0, node_fs_1.existsSync)(recoveryPath)) {
273
- return launchError('stale-state-ambiguous', 'Residual Fleet startup has a prior recovery artifact; inspect it manually.');
273
+ return launchError('stale-state-ambiguous', `Residual Fleet startup has a prior recovery artifact; ebk will not replace it. Inspect the .recover file in sessions/${prepared.request.fleetId} of the per-user EBK application-data directory read-only (any actor may). If it is yours and no ebk dev process for it is running, remove that artifact, then rerun the command.`);
274
274
  }
275
275
  try {
276
276
  (0, node_fs_1.renameSync)(prepared.paths.startup, recoveryPath);
@@ -4,9 +4,17 @@ export interface HumanSecretCeremony {
4
4
  readonly [HUMAN_SECRET_CEREMONY]: true;
5
5
  actor: 'human';
6
6
  agentDelegated: boolean;
7
+ /** Name (never value) of the first agent or CI marker variable detected. */
8
+ detectedMarker?: string;
7
9
  interactive: boolean;
8
10
  stdinIsTty: boolean;
9
11
  }
12
+ export type PersonalSecretOperation = 'remove' | 'set';
13
+ interface HumanRequiredDetail {
14
+ detectedMarker?: string;
15
+ operation: PersonalSecretOperation;
16
+ reason: 'agent-or-ci' | 'capability' | 'terminal';
17
+ }
10
18
  export interface HiddenSecretInputReader {
11
19
  readHidden(input: {
12
20
  logicalName: string;
@@ -49,7 +57,7 @@ export interface PersonalSecretCommandResult {
49
57
  }
50
58
  export declare class PersonalSecretCommandError extends Error {
51
59
  readonly code: 'personal-secret-account-invalid' | 'personal-secret-command-invalid' | 'personal-secret-confirmation-required' | 'personal-secret-hidden-input-failed' | 'personal-secret-human-required' | 'personal-secret-stage-invalid' | 'personal-secret-store-failed' | 'personal-secret-value-invalid';
52
- constructor(code: PersonalSecretCommandError['code']);
60
+ constructor(code: PersonalSecretCommandError['code'], detail?: HumanRequiredDetail);
53
61
  }
54
62
  interface PersonalSecretEnvironment {
55
63
  accountId: string;
@@ -74,10 +82,18 @@ export interface RemovePersonalSecretInput extends PersonalSecretEnvironment {
74
82
  */
75
83
  export declare function issueHumanSecretCeremony(input: {
76
84
  agentDelegated: boolean;
85
+ detectedMarker?: string;
77
86
  interactive: boolean;
78
87
  stdinIsTty: boolean;
79
88
  }): HumanSecretCeremony;
80
- export declare function assertHumanSecretCeremony(ceremony: HumanSecretCeremony): void;
89
+ /**
90
+ * `set` carries a human-supplied value, so it refuses every agent or CI
91
+ * marker: a real TTY proves nothing when an agent owns the terminal. `remove`
92
+ * carries no value and only targets the human's personal development stage,
93
+ * so an agent-delegated session may start it; it still needs a local
94
+ * interactive terminal where the human types the hidden confirmation.
95
+ */
96
+ export declare function assertHumanSecretCeremony(ceremony: HumanSecretCeremony, operation: PersonalSecretOperation): void;
81
97
  export declare function setPersonalSstSecret(input: SetPersonalSecretInput): Promise<PersonalSecretCommandResult>;
82
98
  export declare function removePersonalSstSecret(input: RemovePersonalSecretInput): Promise<PersonalSecretCommandResult>;
83
99
  export {};
@@ -8,10 +8,33 @@ exports.removePersonalSstSecret = removePersonalSstSecret;
8
8
  const stages_js_1 = require("../foundation/stages.js");
9
9
  const model_js_1 = require("./model.js");
10
10
  const HUMAN_SECRET_CEREMONY = Symbol('ebk.human-secret-ceremony');
11
+ const markerNamePattern = /^[A-Z][A-Z0-9_]{0,63}$/;
12
+ function humanRequiredMessage(detail) {
13
+ const code = '(personal-secret-human-required)';
14
+ if (detail.reason === 'agent-or-ci') {
15
+ return [
16
+ `ebk secret set must be run by a human in a terminal that no agent or CI job spawned ${code},`,
17
+ 'because the hidden prompt does not hide the value from the process that owns this terminal.',
18
+ 'Ask the human to open their own terminal and run the same command;',
19
+ 'agents may use the value-blind secret-metadata and status targets instead.',
20
+ ...(detail.detectedMarker
21
+ ? [`Detected agent/CI marker: ${detail.detectedMarker}.`]
22
+ : []),
23
+ ].join(' ');
24
+ }
25
+ if (detail.reason === 'terminal') {
26
+ return detail.operation === 'set'
27
+ ? `ebk secret set needs a local interactive terminal for its hidden value prompt ${code}. Run it in a human-operated terminal that no agent or CI job spawned.`
28
+ : `ebk secret remove needs a local interactive terminal where the human types the hidden REMOVE <logical-name> confirmation ${code}. An agent may start it only in a terminal the human can type into.`;
29
+ }
30
+ return `Personal secret command failed ${code}.`;
31
+ }
11
32
  class PersonalSecretCommandError extends Error {
12
33
  code;
13
- constructor(code) {
14
- super(`Personal secret command failed (${code}).`);
34
+ constructor(code, detail) {
35
+ super(code === 'personal-secret-human-required' && detail
36
+ ? humanRequiredMessage(detail)
37
+ : `Personal secret command failed (${code}).`);
15
38
  this.name = 'PersonalSecretCommandError';
16
39
  this.code = code;
17
40
  }
@@ -24,7 +47,7 @@ function rejectUnexpectedProperties(input, allowed) {
24
47
  throw new PersonalSecretCommandError('personal-secret-command-invalid');
25
48
  }
26
49
  }
27
- function validateEnvironment(input) {
50
+ function validateEnvironment(input, operation) {
28
51
  try {
29
52
  (0, model_js_1.validateSecretMetadataDeclaration)(input.declaration);
30
53
  }
@@ -42,7 +65,7 @@ function validateEnvironment(input) {
42
65
  input.accountId !== input.developmentAccountId) {
43
66
  throw new PersonalSecretCommandError('personal-secret-account-invalid');
44
67
  }
45
- assertHumanSecretCeremony(input.ceremony);
68
+ assertHumanSecretCeremony(input.ceremony, operation);
46
69
  }
47
70
  /**
48
71
  * Issues the in-process capability used by the CLI after it has inspected the
@@ -52,12 +75,19 @@ function validateEnvironment(input) {
52
75
  function issueHumanSecretCeremony(input) {
53
76
  if (typeof input.agentDelegated !== 'boolean' ||
54
77
  typeof input.interactive !== 'boolean' ||
55
- typeof input.stdinIsTty !== 'boolean') {
78
+ typeof input.stdinIsTty !== 'boolean' ||
79
+ (input.detectedMarker !== undefined &&
80
+ (!input.agentDelegated ||
81
+ typeof input.detectedMarker !== 'string' ||
82
+ !markerNamePattern.test(input.detectedMarker)))) {
56
83
  throw new PersonalSecretCommandError('personal-secret-human-required');
57
84
  }
58
85
  const ceremony = {
59
86
  actor: 'human',
60
87
  agentDelegated: input.agentDelegated,
88
+ ...(input.detectedMarker === undefined
89
+ ? {}
90
+ : { detectedMarker: input.detectedMarker }),
61
91
  interactive: input.interactive,
62
92
  stdinIsTty: input.stdinIsTty,
63
93
  };
@@ -69,15 +99,39 @@ function issueHumanSecretCeremony(input) {
69
99
  });
70
100
  return Object.freeze(ceremony);
71
101
  }
72
- function assertHumanSecretCeremony(ceremony) {
102
+ /**
103
+ * `set` carries a human-supplied value, so it refuses every agent or CI
104
+ * marker: a real TTY proves nothing when an agent owns the terminal. `remove`
105
+ * carries no value and only targets the human's personal development stage,
106
+ * so an agent-delegated session may start it; it still needs a local
107
+ * interactive terminal where the human types the hidden confirmation.
108
+ */
109
+ function assertHumanSecretCeremony(ceremony, operation) {
73
110
  if (typeof ceremony !== 'object' ||
74
111
  ceremony === null ||
75
112
  ceremony[HUMAN_SECRET_CEREMONY] !== true ||
76
113
  ceremony.actor !== 'human' ||
77
- ceremony.agentDelegated ||
78
- !ceremony.interactive ||
79
- !ceremony.stdinIsTty) {
80
- throw new PersonalSecretCommandError('personal-secret-human-required');
114
+ (operation !== 'set' && operation !== 'remove')) {
115
+ throw new PersonalSecretCommandError('personal-secret-human-required', {
116
+ operation: operation === 'remove' ? 'remove' : 'set',
117
+ reason: 'capability',
118
+ });
119
+ }
120
+ if (operation === 'set' && ceremony.agentDelegated !== false) {
121
+ throw new PersonalSecretCommandError('personal-secret-human-required', {
122
+ ...(typeof ceremony.detectedMarker === 'string' &&
123
+ markerNamePattern.test(ceremony.detectedMarker)
124
+ ? { detectedMarker: ceremony.detectedMarker }
125
+ : {}),
126
+ operation,
127
+ reason: 'agent-or-ci',
128
+ });
129
+ }
130
+ if (ceremony.interactive !== true || ceremony.stdinIsTty !== true) {
131
+ throw new PersonalSecretCommandError('personal-secret-human-required', {
132
+ operation,
133
+ reason: 'terminal',
134
+ });
81
135
  }
82
136
  }
83
137
  async function setPersonalSstSecret(input) {
@@ -92,7 +146,7 @@ async function setPersonalSstSecret(input) {
92
146
  'store',
93
147
  'validator',
94
148
  ]);
95
- validateEnvironment(input);
149
+ validateEnvironment(input, 'set');
96
150
  let bytes;
97
151
  try {
98
152
  bytes = await input.hiddenInput.readHidden({
@@ -163,7 +217,7 @@ async function removePersonalSstSecret(input) {
163
217
  'stage',
164
218
  'store',
165
219
  ]);
166
- validateEnvironment(input);
220
+ validateEnvironment(input, 'remove');
167
221
  let confirmed;
168
222
  try {
169
223
  confirmed = await input.confirmation.confirm({
@@ -87,7 +87,12 @@ async function collectSourceFiles(projectRoot, directory, declaredSecretNames) {
87
87
  ...(path === 'scripts/remove-personal-secrets.mjs'
88
88
  ? { allowFrameworkPersonalSstRemove: true }
89
89
  : {}),
90
- ...(['jobs/bootstrap-database.mjs', 'jobs/retire-database.mjs'].includes(path)
90
+ ...([
91
+ 'authorization/machine-client-provisioner.spec.ts',
92
+ 'authorization/machine-client-provisioner.ts',
93
+ 'jobs/bootstrap-database.mjs',
94
+ 'jobs/retire-database.mjs',
95
+ ].includes(path)
91
96
  ? { allowFrameworkSecretProviderClient: true }
92
97
  : {}),
93
98
  ...(path === 'infra/personal-secrets.ts'