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

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 (56) 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-testing-persistence-event-files.d.ts +7 -0
  46. package/src/generators/slice/built-in-plan-testing-persistence-event-files.js +487 -0
  47. package/src/generators/slice/built-in-plan-testing-persistence-files.js +2 -0
  48. package/src/generators/slice/built-in-plan.js +3 -1
  49. package/src/generators/workload/frozen-container.d.ts +1 -1
  50. package/src/git-policy/github-bootstrap.d.ts +9 -0
  51. package/src/git-policy/github-bootstrap.js +16 -2
  52. package/src/local-dev/fleet-inventory.js +18 -10
  53. package/src/local-dev/fleet-launcher.js +2 -2
  54. package/src/secrets/personal-command.d.ts +18 -2
  55. package/src/secrets/personal-command.js +66 -12
  56. package/src/secrets/verification.js +6 -1
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderTestingPersistenceFiles = renderTestingPersistenceFiles;
4
+ const built_in_plan_testing_persistence_event_files_js_1 = require("./built-in-plan-testing-persistence-event-files.js");
4
5
  /** Disposable integration resources are independent of fleet or deployed data. */
5
6
  function renderTestingPersistenceFiles(name, pascalName, sql) {
6
7
  const labels = ` labels:
@@ -173,6 +174,7 @@ describe('${name} optional SQL migrations', () => {
173
174
  `,
174
175
  }
175
176
  : {}),
177
+ ...(0, built_in_plan_testing_persistence_event_files_js_1.renderTestingPersistenceEventFiles)(name, pascalName, sql),
176
178
  };
177
179
  }
178
180
  //# sourceMappingURL=built-in-plan-testing-persistence-files.js.map
@@ -239,7 +239,8 @@ function classifyPaths(files, sql) {
239
239
  'api/generated/contract.json',
240
240
  'api/generated/openapi.json',
241
241
  'authorization/machine-authorization-proof.ts',
242
- 'authorization/machine-client-provisioner.py',
242
+ 'authorization/machine-client-provisioner.spec.ts',
243
+ 'authorization/machine-client-provisioner.ts',
243
244
  'authorization/runtime.ts',
244
245
  'authorization/record-store.ts',
245
246
  'authorization/groups.mjs',
@@ -250,6 +251,7 @@ function classifyPaths(files, sql) {
250
251
  'testing/quarantine-runtime.mjs',
251
252
  'testing/strict-json-reporter.mjs',
252
253
  'testing/local-key-value.ts',
254
+ 'testing/local-sql.ts',
253
255
  'e2e/frontend-accessibility.spec.ts',
254
256
  ]);
255
257
  for (const path of built_in_plan_performance_files_js_1.PRODUCT_PERFORMANCE_SEEDED_PATHS)
@@ -1,4 +1,4 @@
1
- export declare const WORKLOAD_CONTAINER_NODE_IMAGE: "node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2";
1
+ export declare const WORKLOAD_CONTAINER_NODE_IMAGE: "node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402";
2
2
  export declare const WORKLOAD_CONTAINER_PNPM_VERSION: "11.20.0";
3
3
  export declare const WORKLOAD_CONTAINER_PNPM_DESCRIPTOR: "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee";
4
4
  export declare const WORKLOAD_CONTAINER_PNPM_STORE = "/workspace/.ebk/cache/pnpm-store";
@@ -15,6 +15,13 @@ export declare const EBK_PROTECTED_MAIN_RULESET_NAME: "EBK protected main (trunk
15
15
  export declare const EBK_STAGING_PROMOTION_PULL_ROLE_SECRET = "AWS_STAGING_PROMOTION_PULL_ROLE_ARN";
16
16
  export declare const EBK_PROTECTED_ENVIRONMENT_SECRETS: readonly ["AWS_ASYNC_RECOVERY_ROLE_ARN", "AWS_ECR_INSPECTOR_EVIDENCE_ROLE_ARN", "AWS_DEPLOY_ROLE_ARN", "AWS_STAGING_PROMOTION_PULL_ROLE_ARN"];
17
17
  export declare const EBK_PROTECTED_ENVIRONMENT_VARIABLES: readonly ["AWS_EXPECTED_ACCOUNT_ID", "AWS_REGION", "AWS_STAGING_PROMOTION_EXPECTED_ACCOUNT_ID", "AWS_STAGING_PROMOTION_REGION", "EBK_ALLOW_PRODUCTION_REMOVE", "EBK_FLEET_ORIGIN"];
18
+ /**
19
+ * Non-secret stage environment variables an operator may set to request a
20
+ * machine credential generation or its retirement on a workflow deploy. They
21
+ * are never required, and a repository- or organization-scoped copy would
22
+ * silently apply to every stage, so only the stage environment may hold them.
23
+ */
24
+ export declare const EBK_OPTIONAL_STAGE_ENVIRONMENT_VARIABLES: readonly ["EBK_MACHINE_CREDENTIAL_GENERATION", "EBK_MACHINE_RETIRE_PREVIOUS_GENERATION"];
18
25
  export declare const EBK_CODE_OWNER_PLACEHOLDER: "@REPLACE_WITH_GITHUB_CODE_OWNER";
19
26
  type GitHubEnvironmentStage = 'development' | 'production' | 'staging';
20
27
  type GitHubAuxiliaryEnvironment = 'async-recovery' | 'ecr-inspector';
@@ -25,6 +32,8 @@ export interface GitHubEnvironmentTopologyEntry {
25
32
  additionalBranches?: readonly ('develop' | 'main')[];
26
33
  branch: 'develop' | 'main';
27
34
  name: GitHubEnvironmentName;
35
+ /** Variables the environment may hold but never requires. */
36
+ optionalVariables?: readonly string[];
28
37
  requiredSecrets: readonly string[];
29
38
  requiredVariables: readonly string[];
30
39
  stage: GitHubEnvironmentStage;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EBK_CODE_OWNER_PLACEHOLDER = exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES = exports.EBK_PROTECTED_ENVIRONMENT_SECRETS = exports.EBK_STAGING_PROMOTION_PULL_ROLE_SECRET = exports.EBK_PROTECTED_MAIN_RULESET_NAME = exports.EBK_RECORD_MAIN_TOKEN_SECRET = exports.EBK_PERFORMANCE_ORIGINS_VARIABLE = exports.EBK_PERFORMANCE_LOAD_ROLE_SECRET = exports.EBK_PERFORMANCE_COST_ROLE_SECRET = exports.EBK_PERFORMANCE_AGGREGATE_ROLE_SECRET = exports.EBK_ASYNC_RECOVERY_ROLE_SECRET = exports.EBK_ECR_INSPECTOR_EVIDENCE_ROLE_SECRET = exports.EBK_RETIRED_DEPLOYMENT_ROLE_SECRET = exports.EBK_REQUIRED_CHECKS = exports.EBK_PROTECTED_BRANCH_RULESET_NAME = void 0;
3
+ exports.EBK_CODE_OWNER_PLACEHOLDER = exports.EBK_OPTIONAL_STAGE_ENVIRONMENT_VARIABLES = exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES = exports.EBK_PROTECTED_ENVIRONMENT_SECRETS = exports.EBK_STAGING_PROMOTION_PULL_ROLE_SECRET = exports.EBK_PROTECTED_MAIN_RULESET_NAME = exports.EBK_RECORD_MAIN_TOKEN_SECRET = exports.EBK_PERFORMANCE_ORIGINS_VARIABLE = exports.EBK_PERFORMANCE_LOAD_ROLE_SECRET = exports.EBK_PERFORMANCE_COST_ROLE_SECRET = exports.EBK_PERFORMANCE_AGGREGATE_ROLE_SECRET = exports.EBK_ASYNC_RECOVERY_ROLE_SECRET = exports.EBK_ECR_INSPECTOR_EVIDENCE_ROLE_SECRET = exports.EBK_RETIRED_DEPLOYMENT_ROLE_SECRET = exports.EBK_REQUIRED_CHECKS = exports.EBK_PROTECTED_BRANCH_RULESET_NAME = void 0;
4
4
  exports.githubEnvironmentTopology = githubEnvironmentTopology;
5
5
  exports.runGitHubBootstrap = runGitHubBootstrap;
6
6
  const index_js_1 = require("../repository-policy/index.js");
@@ -33,6 +33,16 @@ exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES = [
33
33
  'EBK_ALLOW_PRODUCTION_REMOVE',
34
34
  'EBK_FLEET_ORIGIN',
35
35
  ];
36
+ /**
37
+ * Non-secret stage environment variables an operator may set to request a
38
+ * machine credential generation or its retirement on a workflow deploy. They
39
+ * are never required, and a repository- or organization-scoped copy would
40
+ * silently apply to every stage, so only the stage environment may hold them.
41
+ */
42
+ exports.EBK_OPTIONAL_STAGE_ENVIRONMENT_VARIABLES = [
43
+ 'EBK_MACHINE_CREDENTIAL_GENERATION',
44
+ 'EBK_MACHINE_RETIRE_PREVIOUS_GENERATION',
45
+ ];
36
46
  const FORBIDDEN_NPM_TOKEN_NAMES = [
37
47
  'NODE_AUTH_TOKEN',
38
48
  'NPM_CONFIG_TOKEN',
@@ -62,6 +72,7 @@ function stageEnvironmentTopology(stage, deploymentProfile) {
62
72
  return Object.freeze({
63
73
  branch: production ? 'main' : 'develop',
64
74
  name: stage,
75
+ optionalVariables: exports.EBK_OPTIONAL_STAGE_ENVIRONMENT_VARIABLES,
65
76
  requiredSecrets: Object.freeze(production
66
77
  ? deploymentProfile === 'starter'
67
78
  ? []
@@ -808,6 +819,7 @@ function evaluate(repository, state, inspectionBlockers, productionApprovalMode,
808
819
  }
809
820
  for (const variable of [
810
821
  ...exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES,
822
+ ...exports.EBK_OPTIONAL_STAGE_ENVIRONMENT_VARIABLES,
811
823
  ...FORBIDDEN_NPM_TOKEN_NAMES,
812
824
  ]) {
813
825
  if (state.repositoryVariableNames.includes(variable)) {
@@ -880,8 +892,10 @@ function evaluate(repository, state, inspectionBlockers, productionApprovalMode,
880
892
  blockers.push(`${environment} environment is missing required variable ${variable}; bootstrap never creates variable values.`);
881
893
  }
882
894
  }
895
+ const optionalVariables = topology.optionalVariables ?? [];
883
896
  for (const variable of current.variableNames) {
884
- if (!requiredVariables.includes(variable)) {
897
+ if (!requiredVariables.includes(variable) &&
898
+ !optionalVariables.includes(variable)) {
885
899
  blockers.push(`${environment} environment exposes ${variable} outside its exact capability boundary.`);
886
900
  }
887
901
  }
@@ -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'