@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.
- package/README.md +26 -10
- package/package.json +3 -3
- package/src/blueprints/external-sandbox.d.ts +1 -1
- package/src/cli/deploy-runtime.d.ts +25 -1
- package/src/cli/deploy-runtime.js +120 -4
- package/src/cli/dev-command.d.ts +2 -0
- package/src/cli/dev-command.js +8 -0
- package/src/cli/main.js +20 -6
- package/src/cli/personal-secret-runtime.d.ts +12 -0
- package/src/cli/personal-secret-runtime.js +18 -3
- package/src/cli/secret-terminal.js +1 -1
- package/src/container-images.d.ts +1 -1
- package/src/container-images.js +1 -1
- package/src/developer-agent/operation-registry.d.ts +10 -0
- package/src/developer-agent/operation-registry.js +60 -13
- package/src/developer-agent/planning.d.ts +8 -1
- package/src/developer-agent/planning.js +23 -3
- package/src/executors/local-deploy/local-approval.js +1 -1
- package/src/executors/sst-lifecycle/governed-native-operation.js +3 -2
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +6 -0
- package/src/executors/sst-lifecycle/run-sst-command.js +3 -33
- package/src/foundation/cost-estimate.d.ts +170 -0
- package/src/foundation/cost-estimate.js +683 -0
- package/src/generators/preset/tool-versions.d.ts +0 -2
- package/src/generators/preset/tool-versions.js +1 -3
- package/src/generators/preset/workspace-files-adopter-skills.js +39 -18
- package/src/generators/preset/workspace-files-developer-agent.js +83 -27
- package/src/generators/preset/workspace-files-shared-operation-claims.d.ts +10 -0
- package/src/generators/preset/workspace-files-shared-operation-claims.js +59 -0
- package/src/generators/preset/workspace-files-stage-authority.js +13 -2
- package/src/generators/preset/workspace-files-thin-deploy.d.ts +10 -0
- package/src/generators/preset/workspace-files-thin-deploy.js +54 -20
- package/src/generators/preset/workspace-files-thin-docs.js +32 -14
- package/src/generators/preset/workspace-files.js +28 -8
- package/src/generators/slice/built-in-plan-api-contract-files.js +7 -4
- package/src/generators/slice/built-in-plan-authorization-files.js +10 -5
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +933 -249
- package/src/generators/slice/built-in-plan-data-files.js +17 -8
- package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js +5 -1
- package/src/generators/slice/built-in-plan-frontend-files.js +7 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js +0 -1
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +8 -3
- package/src/generators/slice/built-in-plan-metadata-files.js +2 -1
- package/src/generators/slice/built-in-plan-reliability-files.js +4 -1
- package/src/generators/slice/built-in-plan-testing-persistence-event-files.d.ts +7 -0
- package/src/generators/slice/built-in-plan-testing-persistence-event-files.js +487 -0
- package/src/generators/slice/built-in-plan-testing-persistence-files.js +2 -0
- package/src/generators/slice/built-in-plan.js +3 -1
- package/src/generators/workload/frozen-container.d.ts +1 -1
- package/src/git-policy/github-bootstrap.d.ts +9 -0
- package/src/git-policy/github-bootstrap.js +16 -2
- package/src/local-dev/fleet-inventory.js +18 -10
- package/src/local-dev/fleet-launcher.js +2 -2
- package/src/secrets/personal-command.d.ts +18 -2
- package/src/secrets/personal-command.js +66 -12
- 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.
|
|
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:
|
|
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', '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
...([
|
|
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'
|