scene-capability-engine 3.6.45 → 3.6.47

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 (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -0
  3. package/README.zh.md +1 -0
  4. package/docs/agent-runtime/symbol-evidence.schema.json +1 -1
  5. package/docs/command-reference.md +8 -0
  6. package/docs/interactive-customization/dialogue-governance-policy-baseline.json +4 -1
  7. package/docs/interactive-customization/embedded-assistant-authorization-dialogue-rules.md +5 -0
  8. package/docs/releases/README.md +2 -0
  9. package/docs/releases/v3.6.46.md +23 -0
  10. package/docs/releases/v3.6.47.md +23 -0
  11. package/docs/sce-business-mode-map.md +2 -1
  12. package/docs/sce-capability-matrix-e2e-example.md +2 -1
  13. package/docs/security-governance-default-baseline.md +2 -0
  14. package/docs/starter-kit/README.md +3 -0
  15. package/docs/zh/releases/README.md +2 -0
  16. package/docs/zh/releases/v3.6.46.md +23 -0
  17. package/docs/zh/releases/v3.6.47.md +23 -0
  18. package/lib/workspace/takeover-baseline.js +293 -1
  19. package/package.json +6 -2
  20. package/scripts/auto-strategy-router.js +231 -0
  21. package/scripts/capability-mapping-report.js +339 -0
  22. package/scripts/check-branding-consistency.js +140 -0
  23. package/scripts/check-sce-tracking.js +54 -0
  24. package/scripts/check-skip-allowlist.js +94 -0
  25. package/scripts/clarification-first-audit.js +322 -0
  26. package/scripts/errorbook-registry-health-gate.js +172 -0
  27. package/scripts/errorbook-release-gate.js +132 -0
  28. package/scripts/failure-attribution-repair.js +317 -0
  29. package/scripts/git-managed-gate.js +464 -0
  30. package/scripts/interactive-approval-event-projection.js +400 -0
  31. package/scripts/interactive-approval-workflow.js +829 -0
  32. package/scripts/interactive-authorization-tier-evaluate.js +413 -0
  33. package/scripts/interactive-change-plan-gate.js +225 -0
  34. package/scripts/interactive-context-bridge.js +617 -0
  35. package/scripts/interactive-customization-loop.js +1690 -0
  36. package/scripts/interactive-dialogue-governance.js +873 -0
  37. package/scripts/interactive-feedback-log.js +253 -0
  38. package/scripts/interactive-flow-smoke.js +238 -0
  39. package/scripts/interactive-flow.js +1059 -0
  40. package/scripts/interactive-governance-report.js +1112 -0
  41. package/scripts/interactive-intent-build.js +707 -0
  42. package/scripts/interactive-loop-smoke.js +215 -0
  43. package/scripts/interactive-moqui-adapter.js +304 -0
  44. package/scripts/interactive-plan-build.js +426 -0
  45. package/scripts/interactive-runtime-policy-evaluate.js +495 -0
  46. package/scripts/interactive-work-order-build.js +552 -0
  47. package/scripts/matrix-regression-gate.js +167 -0
  48. package/scripts/moqui-core-regression-suite.js +397 -0
  49. package/scripts/moqui-lexicon-audit.js +651 -0
  50. package/scripts/moqui-matrix-remediation-phased-runner.js +865 -0
  51. package/scripts/moqui-matrix-remediation-queue.js +852 -0
  52. package/scripts/moqui-metadata-extract.js +1340 -0
  53. package/scripts/moqui-rebuild-gate.js +167 -0
  54. package/scripts/moqui-release-summary.js +729 -0
  55. package/scripts/moqui-standard-rebuild.js +1370 -0
  56. package/scripts/moqui-template-baseline-report.js +682 -0
  57. package/scripts/npm-package-runtime-asset-check.js +221 -0
  58. package/scripts/problem-closure-gate.js +441 -0
  59. package/scripts/release-asset-integrity-check.js +216 -0
  60. package/scripts/release-asset-nonempty-normalize.js +166 -0
  61. package/scripts/release-drift-evaluate.js +223 -0
  62. package/scripts/release-drift-signals.js +255 -0
  63. package/scripts/release-governance-snapshot-export.js +132 -0
  64. package/scripts/release-ops-weekly-summary.js +934 -0
  65. package/scripts/release-risk-remediation-bundle.js +315 -0
  66. package/scripts/release-weekly-ops-gate.js +423 -0
  67. package/scripts/state-migration-reconciliation-gate.js +110 -0
  68. package/scripts/state-storage-tiering-audit.js +337 -0
  69. package/scripts/steering-content-audit.js +393 -0
  70. package/scripts/symbol-evidence-locate.js +370 -0
  71. package/template/.sce/README.md +1 -0
  72. package/template/.sce/steering/CORE_PRINCIPLES.md +25 -0
@@ -0,0 +1,829 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+ const crypto = require('crypto');
7
+
8
+ const DEFAULT_STATE_FILE = '.sce/reports/interactive-approval-state.json';
9
+ const DEFAULT_AUDIT_FILE = '.sce/reports/interactive-approval-events.jsonl';
10
+ const DEFAULT_PASSWORD_HASH_ENV = 'SCE_INTERACTIVE_AUTH_PASSWORD_SHA256';
11
+ const DEFAULT_PASSWORD_TTL_SECONDS = 600;
12
+ const PASSWORD_SCOPES = new Set(['approve', 'execute']);
13
+ const WORKFLOW_ACTIONS = new Set([
14
+ 'init',
15
+ 'submit',
16
+ 'approve',
17
+ 'reject',
18
+ 'execute',
19
+ 'verify',
20
+ 'archive',
21
+ 'status'
22
+ ]);
23
+ const ROLE_SCOPED_ACTIONS = new Set(['submit', 'approve', 'reject', 'execute', 'verify', 'archive']);
24
+
25
+ function parseArgs(argv) {
26
+ const options = {
27
+ action: null,
28
+ plan: null,
29
+ stateFile: DEFAULT_STATE_FILE,
30
+ auditFile: DEFAULT_AUDIT_FILE,
31
+ actor: null,
32
+ comment: null,
33
+ force: false,
34
+ actorRole: null,
35
+ rolePolicy: null,
36
+ password: null,
37
+ passwordHash: null,
38
+ passwordHashEnv: null,
39
+ passwordRequired: false,
40
+ passwordScope: null,
41
+ passwordTtlSeconds: null,
42
+ json: false
43
+ };
44
+
45
+ for (let i = 0; i < argv.length; i += 1) {
46
+ const token = argv[i];
47
+ const next = argv[i + 1];
48
+ if (token === '--action' && next) {
49
+ options.action = next;
50
+ i += 1;
51
+ } else if (token === '--plan' && next) {
52
+ options.plan = next;
53
+ i += 1;
54
+ } else if (token === '--state-file' && next) {
55
+ options.stateFile = next;
56
+ i += 1;
57
+ } else if (token === '--audit-file' && next) {
58
+ options.auditFile = next;
59
+ i += 1;
60
+ } else if (token === '--actor' && next) {
61
+ options.actor = next;
62
+ i += 1;
63
+ } else if (token === '--comment' && next) {
64
+ options.comment = next;
65
+ i += 1;
66
+ } else if (token === '--actor-role' && next) {
67
+ options.actorRole = next;
68
+ i += 1;
69
+ } else if (token === '--role-policy' && next) {
70
+ options.rolePolicy = next;
71
+ i += 1;
72
+ } else if (token === '--force') {
73
+ options.force = true;
74
+ } else if (token === '--password' && next) {
75
+ options.password = next;
76
+ i += 1;
77
+ } else if (token === '--password-hash' && next) {
78
+ options.passwordHash = next;
79
+ i += 1;
80
+ } else if (token === '--password-hash-env' && next) {
81
+ options.passwordHashEnv = next;
82
+ i += 1;
83
+ } else if (token === '--password-required') {
84
+ options.passwordRequired = true;
85
+ } else if (token === '--password-scope' && next) {
86
+ options.passwordScope = next;
87
+ i += 1;
88
+ } else if (token === '--password-ttl-seconds' && next) {
89
+ options.passwordTtlSeconds = Number(next);
90
+ i += 1;
91
+ } else if (token === '--json') {
92
+ options.json = true;
93
+ } else if (token === '--help' || token === '-h') {
94
+ printHelpAndExit(0);
95
+ }
96
+ }
97
+
98
+ if (!options.action) {
99
+ throw new Error('--action is required.');
100
+ }
101
+ const action = `${options.action}`.trim().toLowerCase();
102
+ if (!WORKFLOW_ACTIONS.has(action)) {
103
+ throw new Error(`--action must be one of: ${Array.from(WORKFLOW_ACTIONS).join(', ')}`);
104
+ }
105
+ options.action = action;
106
+
107
+ if (action === 'init' && !options.plan) {
108
+ throw new Error('--plan is required for --action init.');
109
+ }
110
+ if (action !== 'status' && `${options.actor || ''}`.trim().length === 0) {
111
+ throw new Error('--actor is required for mutating actions.');
112
+ }
113
+
114
+ if (options.passwordHash && !isSha256Hash(options.passwordHash)) {
115
+ throw new Error('--password-hash must be a sha256 hex string (64 chars).');
116
+ }
117
+ if (options.passwordHashEnv != null && `${options.passwordHashEnv || ''}`.trim().length === 0) {
118
+ throw new Error('--password-hash-env cannot be empty.');
119
+ }
120
+ if (options.actorRole != null && `${options.actorRole || ''}`.trim().length === 0) {
121
+ throw new Error('--actor-role cannot be empty.');
122
+ }
123
+ if (options.rolePolicy != null && `${options.rolePolicy || ''}`.trim().length === 0) {
124
+ throw new Error('--role-policy cannot be empty.');
125
+ }
126
+ if (options.passwordScope != null) {
127
+ options.passwordScope = parsePasswordScope(options.passwordScope, '--password-scope');
128
+ }
129
+ if (options.passwordTtlSeconds != null) {
130
+ if (!Number.isFinite(options.passwordTtlSeconds) || options.passwordTtlSeconds <= 0) {
131
+ throw new Error('--password-ttl-seconds must be a positive number.');
132
+ }
133
+ }
134
+
135
+ options.passwordHash = options.passwordHash ? options.passwordHash.trim().toLowerCase() : null;
136
+ options.passwordHashEnv = options.passwordHashEnv ? options.passwordHashEnv.trim() : null;
137
+ options.actorRole = options.actorRole ? options.actorRole.trim().toLowerCase() : null;
138
+ options.rolePolicy = options.rolePolicy ? options.rolePolicy.trim() : null;
139
+ return options;
140
+ }
141
+
142
+ function printHelpAndExit(code) {
143
+ const lines = [
144
+ 'Usage: node scripts/interactive-approval-workflow.js --action <name> [options]',
145
+ '',
146
+ 'Actions:',
147
+ ' init, submit, approve, reject, execute, verify, archive, status',
148
+ '',
149
+ 'Options:',
150
+ ' --action <name> Workflow action (required)',
151
+ ' --plan <path> Change plan JSON (required for init)',
152
+ ` --state-file <path> Workflow state JSON file (default: ${DEFAULT_STATE_FILE})`,
153
+ ` --audit-file <path> Workflow events JSONL file (default: ${DEFAULT_AUDIT_FILE})`,
154
+ ' --actor <id> Actor identifier (required for mutating actions)',
155
+ ' --actor-role <name> Actor role identifier for role-policy checks',
156
+ ' --role-policy <path> Role policy JSON path (optional; enables role checks)',
157
+ ' --comment <text> Optional action comment',
158
+ ' --force Allow init to overwrite existing state file',
159
+ ' --password <text> One-time password input (for password-protected actions)',
160
+ ' --password-hash <sha256> Password verifier hash override for init',
161
+ ` --password-hash-env <name> Environment variable that stores password hash (default: ${DEFAULT_PASSWORD_HASH_ENV})`,
162
+ ' --password-required Force password authorization requirement in init',
163
+ ' --password-scope <csv> Password scope override: approve,execute',
164
+ ` --password-ttl-seconds <n> Password verification TTL seconds (default: ${DEFAULT_PASSWORD_TTL_SECONDS})`,
165
+ ' --json Print JSON payload',
166
+ ' -h, --help Show this help'
167
+ ];
168
+ console.log(lines.join('\n'));
169
+ process.exit(code);
170
+ }
171
+
172
+ function resolvePath(cwd, value) {
173
+ return path.isAbsolute(value) ? value : path.resolve(cwd, value);
174
+ }
175
+
176
+ async function readJsonFile(filePath, label) {
177
+ if (!(await fs.pathExists(filePath))) {
178
+ throw new Error(`${label} not found: ${filePath}`);
179
+ }
180
+ const content = await fs.readFile(filePath, 'utf8');
181
+ try {
182
+ return JSON.parse(content);
183
+ } catch (error) {
184
+ throw new Error(`invalid JSON in ${label}: ${error.message}`);
185
+ }
186
+ }
187
+
188
+ async function resolveRoleRequirements(cwd, options, plan) {
189
+ let fromPolicy = {};
190
+ if (options.rolePolicy) {
191
+ const rolePolicyPath = resolvePath(cwd, options.rolePolicy);
192
+ const rolePolicy = await readJsonFile(rolePolicyPath, 'role-policy');
193
+ const policyRoles = rolePolicy && rolePolicy.role_requirements && typeof rolePolicy.role_requirements === 'object'
194
+ ? rolePolicy.role_requirements
195
+ : {};
196
+ fromPolicy = normalizeRoleRequirements(policyRoles);
197
+ }
198
+
199
+ const planAuthorization = plan && plan.authorization && typeof plan.authorization === 'object'
200
+ ? plan.authorization
201
+ : {};
202
+ const fromPlan = normalizeRoleRequirements(planAuthorization.role_requirements || {});
203
+ return mergeRoleRequirements(fromPolicy, fromPlan);
204
+ }
205
+
206
+ function normalizeRiskLevel(value) {
207
+ const normalized = `${value || ''}`.trim().toLowerCase();
208
+ return ['low', 'medium', 'high'].includes(normalized) ? normalized : 'medium';
209
+ }
210
+
211
+ function createEvent(state, action, actor, actorRole, comment, fromStatus, toStatus, blocked, reason) {
212
+ return {
213
+ event_id: `event-${crypto.randomUUID()}`,
214
+ workflow_id: state && state.workflow_id ? state.workflow_id : null,
215
+ event_type: blocked ? 'interactive.approval.blocked' : `interactive.approval.${action}`,
216
+ action,
217
+ actor,
218
+ actor_role: actorRole || null,
219
+ comment: comment || null,
220
+ from_status: fromStatus || null,
221
+ to_status: toStatus || null,
222
+ blocked: blocked === true,
223
+ reason: reason || null,
224
+ timestamp: new Date().toISOString()
225
+ };
226
+ }
227
+
228
+ function toBoolean(value) {
229
+ return value === true;
230
+ }
231
+
232
+ function isSha256Hash(value) {
233
+ return /^[a-fA-F0-9]{64}$/.test(`${value || ''}`.trim());
234
+ }
235
+
236
+ function sha256(value) {
237
+ return crypto.createHash('sha256').update(String(value)).digest('hex');
238
+ }
239
+
240
+ function parsePasswordScope(value, label = 'password scope') {
241
+ const tokens = `${value || ''}`
242
+ .split(',')
243
+ .map(item => item.trim().toLowerCase())
244
+ .filter(Boolean);
245
+ const unique = Array.from(new Set(tokens));
246
+ const invalid = unique.filter(item => !PASSWORD_SCOPES.has(item));
247
+ if (invalid.length > 0) {
248
+ throw new Error(`${label} supports only: approve, execute`);
249
+ }
250
+ return unique;
251
+ }
252
+
253
+ function normalizePasswordScope(value, fallback = []) {
254
+ if (Array.isArray(value)) {
255
+ const normalized = value
256
+ .map(item => `${item || ''}`.trim().toLowerCase())
257
+ .filter(item => PASSWORD_SCOPES.has(item));
258
+ return Array.from(new Set(normalized));
259
+ }
260
+ if (typeof value === 'string' && value.trim()) {
261
+ try {
262
+ return parsePasswordScope(value);
263
+ } catch (_error) {
264
+ return [...fallback];
265
+ }
266
+ }
267
+ return [...fallback];
268
+ }
269
+
270
+ function normalizeRoleName(value) {
271
+ return `${value || ''}`.trim().toLowerCase();
272
+ }
273
+
274
+ function normalizeRoleList(value) {
275
+ if (Array.isArray(value)) {
276
+ return Array.from(new Set(value.map(item => normalizeRoleName(item)).filter(Boolean)));
277
+ }
278
+ if (typeof value === 'string' && value.trim()) {
279
+ return Array.from(new Set(
280
+ value
281
+ .split(',')
282
+ .map(item => normalizeRoleName(item))
283
+ .filter(Boolean)
284
+ ));
285
+ }
286
+ return [];
287
+ }
288
+
289
+ function normalizeRoleRequirements(value = {}) {
290
+ if (!value || typeof value !== 'object') {
291
+ return {};
292
+ }
293
+ const normalized = {};
294
+ for (const key of Object.keys(value)) {
295
+ const action = normalizeRoleName(key);
296
+ if (!ROLE_SCOPED_ACTIONS.has(action)) {
297
+ continue;
298
+ }
299
+ const roles = normalizeRoleList(value[key]);
300
+ if (roles.length > 0) {
301
+ normalized[action] = roles;
302
+ }
303
+ }
304
+ return normalized;
305
+ }
306
+
307
+ function mergeRoleRequirements(base = {}, override = {}) {
308
+ const merged = { ...base };
309
+ const normalizedOverride = normalizeRoleRequirements(override);
310
+ for (const action of Object.keys(normalizedOverride)) {
311
+ merged[action] = normalizedOverride[action];
312
+ }
313
+ return merged;
314
+ }
315
+
316
+ function resolvePasswordTtlSeconds(optionsTtl, planTtl) {
317
+ const fromOptions = Number(optionsTtl);
318
+ if (Number.isFinite(fromOptions) && fromOptions > 0) {
319
+ return Math.floor(fromOptions);
320
+ }
321
+ const fromPlan = Number(planTtl);
322
+ if (Number.isFinite(fromPlan) && fromPlan > 0) {
323
+ return Math.floor(fromPlan);
324
+ }
325
+ return DEFAULT_PASSWORD_TTL_SECONDS;
326
+ }
327
+
328
+ function isMutatingPlanAction(item) {
329
+ return `${item && item.type ? item.type : ''}`.trim().toLowerCase() !== 'analysis_only';
330
+ }
331
+
332
+ function computeApprovalFlags(plan) {
333
+ const actions = Array.isArray(plan && plan.actions)
334
+ ? plan.actions.filter(item => item && typeof item === 'object')
335
+ : [];
336
+ const riskLevel = normalizeRiskLevel(plan && plan.risk_level);
337
+ const privilegeEscalationDetected = actions.some(item => item.requires_privilege_escalation === true);
338
+ const approvalRequired = (
339
+ riskLevel === 'high' ||
340
+ privilegeEscalationDetected ||
341
+ (plan && plan.approval && plan.approval.status === 'pending') ||
342
+ (plan && plan.approval && plan.approval.status === 'approved')
343
+ );
344
+ const dualApprovalRequired = toBoolean(
345
+ plan && plan.approval && plan.approval.dual_approved === false && privilegeEscalationDetected
346
+ );
347
+ return {
348
+ approval_required: approvalRequired,
349
+ dual_approval_required: dualApprovalRequired,
350
+ privilege_escalation_detected: privilegeEscalationDetected
351
+ };
352
+ }
353
+
354
+ function computeAuthorization(plan, options = {}) {
355
+ const authorization = plan && plan.authorization && typeof plan.authorization === 'object'
356
+ ? plan.authorization
357
+ : {};
358
+ const actions = Array.isArray(plan && plan.actions)
359
+ ? plan.actions.filter(item => item && typeof item === 'object')
360
+ : [];
361
+ const executionMode = `${plan && plan.execution_mode ? plan.execution_mode : ''}`.trim().toLowerCase();
362
+ const mutating = actions.some(item => isMutatingPlanAction(item));
363
+
364
+ const passwordRequired = (
365
+ options.passwordRequired === true ||
366
+ authorization.password_required === true ||
367
+ (executionMode === 'apply' && mutating)
368
+ );
369
+ const passwordScope = passwordRequired
370
+ ? (
371
+ (Array.isArray(options.passwordScope) && options.passwordScope.length > 0)
372
+ ? [...options.passwordScope]
373
+ : normalizePasswordScope(authorization.password_scope, ['execute'])
374
+ )
375
+ : [];
376
+ const passwordHash = options.passwordHash
377
+ ? options.passwordHash
378
+ : (isSha256Hash(authorization.password_hash) ? String(authorization.password_hash).trim().toLowerCase() : null);
379
+ const passwordHashEnv = options.passwordHashEnv
380
+ ? options.passwordHashEnv
381
+ : `${authorization.password_hash_env || DEFAULT_PASSWORD_HASH_ENV}`.trim() || DEFAULT_PASSWORD_HASH_ENV;
382
+ const passwordTtlSeconds = resolvePasswordTtlSeconds(options.passwordTtlSeconds, authorization.password_ttl_seconds);
383
+ const reasonCodes = Array.isArray(authorization.reason_codes)
384
+ ? authorization.reason_codes.map(item => `${item || ''}`.trim()).filter(Boolean)
385
+ : [];
386
+ const roleRequirements = normalizeRoleRequirements(options.roleRequirements || authorization.role_requirements || {});
387
+
388
+ return {
389
+ password_required: passwordRequired,
390
+ password_scope: passwordScope,
391
+ password_hash: passwordHash,
392
+ password_hash_env: passwordHashEnv,
393
+ password_ttl_seconds: passwordTtlSeconds,
394
+ password_verified: false,
395
+ password_verified_at: null,
396
+ password_verified_by: null,
397
+ password_expires_at: null,
398
+ reason_codes: reasonCodes,
399
+ role_requirements: roleRequirements
400
+ };
401
+ }
402
+
403
+ function buildInitialState(plan, actor, comment, options) {
404
+ const now = new Date().toISOString();
405
+ const flags = computeApprovalFlags(plan);
406
+ const initial = {
407
+ mode: 'interactive-approval-workflow',
408
+ workflow_id: `wf-${crypto.randomUUID()}`,
409
+ plan_id: plan.plan_id || null,
410
+ intent_id: plan.intent_id || null,
411
+ risk_level: normalizeRiskLevel(plan.risk_level),
412
+ execution_mode: `${plan.execution_mode || 'suggestion'}`.trim() || 'suggestion',
413
+ approval_required: flags.approval_required,
414
+ dual_approval_required: flags.dual_approval_required,
415
+ privilege_escalation_detected: flags.privilege_escalation_detected,
416
+ status: 'draft',
417
+ approvals: {
418
+ status: flags.approval_required ? 'pending' : 'not-required',
419
+ approvers: [],
420
+ rejected_by: null
421
+ },
422
+ authorization: computeAuthorization(plan, options),
423
+ history: [],
424
+ created_at: now,
425
+ updated_at: now
426
+ };
427
+ const event = createEvent(
428
+ initial,
429
+ 'init',
430
+ actor,
431
+ options && options.actorRole ? options.actorRole : null,
432
+ comment,
433
+ null,
434
+ 'draft',
435
+ false,
436
+ null
437
+ );
438
+ initial.history.push(event);
439
+ return { state: initial, event };
440
+ }
441
+
442
+ function assertTransition(state, fromList, action) {
443
+ if (!fromList.includes(state.status)) {
444
+ return {
445
+ ok: false,
446
+ reason: `cannot ${action} when status is ${state.status}; expected ${fromList.join('|')}`
447
+ };
448
+ }
449
+ return { ok: true, reason: null };
450
+ }
451
+
452
+ function resolveVerifierHash(state, env = process.env) {
453
+ const authorization = state && state.authorization && typeof state.authorization === 'object'
454
+ ? state.authorization
455
+ : {};
456
+ if (authorization.password_hash && isSha256Hash(authorization.password_hash)) {
457
+ return String(authorization.password_hash).trim().toLowerCase();
458
+ }
459
+ const envName = `${authorization.password_hash_env || DEFAULT_PASSWORD_HASH_ENV}`.trim();
460
+ const fromEnv = env && Object.prototype.hasOwnProperty.call(env, envName)
461
+ ? `${env[envName] || ''}`.trim().toLowerCase()
462
+ : '';
463
+ if (isSha256Hash(fromEnv)) {
464
+ return fromEnv;
465
+ }
466
+ return null;
467
+ }
468
+
469
+ function isPasswordVerificationActive(authorization, nowMs) {
470
+ if (!authorization || authorization.password_verified !== true) {
471
+ return false;
472
+ }
473
+ const expires = authorization.password_expires_at
474
+ ? Date.parse(authorization.password_expires_at)
475
+ : Number.NaN;
476
+ if (!Number.isFinite(expires)) {
477
+ return false;
478
+ }
479
+ return nowMs < expires;
480
+ }
481
+
482
+ function requirePasswordForAction(state, options, action, nowIso) {
483
+ const authorization = state && state.authorization && typeof state.authorization === 'object'
484
+ ? state.authorization
485
+ : {};
486
+ const scope = normalizePasswordScope(authorization.password_scope, []);
487
+ if (authorization.password_required !== true || !scope.includes(action)) {
488
+ return { ok: true, reason: null };
489
+ }
490
+
491
+ const nowMs = Date.parse(nowIso);
492
+ if (isPasswordVerificationActive(authorization, nowMs)) {
493
+ return { ok: true, reason: null };
494
+ }
495
+
496
+ const verifierHash = resolveVerifierHash(state, process.env);
497
+ if (!verifierHash) {
498
+ return {
499
+ ok: false,
500
+ reason: 'password authorization required but verifier hash is not configured'
501
+ };
502
+ }
503
+
504
+ const candidate = `${options.password || ''}`;
505
+ if (!candidate.trim()) {
506
+ return {
507
+ ok: false,
508
+ reason: `password authorization required for ${action}`
509
+ };
510
+ }
511
+
512
+ const candidateHash = sha256(candidate).toLowerCase();
513
+ if (candidateHash !== verifierHash) {
514
+ return {
515
+ ok: false,
516
+ reason: 'password authorization failed'
517
+ };
518
+ }
519
+
520
+ const ttlSeconds = resolvePasswordTtlSeconds(
521
+ authorization.password_ttl_seconds,
522
+ DEFAULT_PASSWORD_TTL_SECONDS
523
+ );
524
+ authorization.password_verified = true;
525
+ authorization.password_verified_by = `${options.actor || ''}`.trim() || null;
526
+ authorization.password_verified_at = nowIso;
527
+ authorization.password_expires_at = new Date(nowMs + (ttlSeconds * 1000)).toISOString();
528
+ state.authorization = authorization;
529
+ return { ok: true, reason: null };
530
+ }
531
+
532
+ function requireRoleForAction(state, options, action) {
533
+ const authorization = state && state.authorization && typeof state.authorization === 'object'
534
+ ? state.authorization
535
+ : {};
536
+ const roleRequirements = authorization && authorization.role_requirements && typeof authorization.role_requirements === 'object'
537
+ ? authorization.role_requirements
538
+ : {};
539
+ const allowedRoles = normalizeRoleList(roleRequirements[action]);
540
+ if (allowedRoles.length === 0) {
541
+ return { ok: true, reason: null };
542
+ }
543
+
544
+ const actorRole = normalizeRoleName(options && options.actorRole);
545
+ if (!actorRole) {
546
+ return {
547
+ ok: false,
548
+ reason: `actor role required for ${action}; allowed roles: ${allowedRoles.join(', ')}`
549
+ };
550
+ }
551
+ if (!allowedRoles.includes(actorRole)) {
552
+ return {
553
+ ok: false,
554
+ reason: `actor role "${actorRole}" is not allowed for ${action}; allowed roles: ${allowedRoles.join(', ')}`
555
+ };
556
+ }
557
+ return { ok: true, reason: null };
558
+ }
559
+
560
+ function mutateStateForAction(state, options) {
561
+ const actor = `${options.actor || ''}`.trim();
562
+ const comment = options.comment || null;
563
+ const action = options.action;
564
+ const now = new Date().toISOString();
565
+ let fromStatus = state.status;
566
+ let toStatus = state.status;
567
+ let blocked = false;
568
+ let reason = null;
569
+
570
+ const fail = (message) => {
571
+ blocked = true;
572
+ reason = message;
573
+ };
574
+
575
+ if (action === 'status') {
576
+ fromStatus = state.status;
577
+ toStatus = state.status;
578
+ } else if (action === 'submit') {
579
+ const check = assertTransition(state, ['draft'], action);
580
+ if (!check.ok) {
581
+ fail(check.reason);
582
+ } else {
583
+ const roleCheck = requireRoleForAction(state, options, action);
584
+ if (!roleCheck.ok) {
585
+ fail(roleCheck.reason);
586
+ } else {
587
+ toStatus = 'submitted';
588
+ state.status = toStatus;
589
+ if (state.approval_required && state.approvals.status === 'not-required') {
590
+ state.approvals.status = 'pending';
591
+ }
592
+ }
593
+ }
594
+ } else if (action === 'approve') {
595
+ const check = assertTransition(state, ['submitted'], action);
596
+ if (!check.ok) {
597
+ fail(check.reason);
598
+ } else {
599
+ const roleCheck = requireRoleForAction(state, options, action);
600
+ if (!roleCheck.ok) {
601
+ fail(roleCheck.reason);
602
+ } else {
603
+ const auth = requirePasswordForAction(state, options, action, now);
604
+ if (!auth.ok) {
605
+ fail(auth.reason);
606
+ } else {
607
+ toStatus = 'approved';
608
+ state.status = toStatus;
609
+ if (!state.approvals.approvers.includes(actor)) {
610
+ state.approvals.approvers.push(actor);
611
+ }
612
+ state.approvals.status = 'approved';
613
+ state.approvals.rejected_by = null;
614
+ }
615
+ }
616
+ }
617
+ } else if (action === 'reject') {
618
+ const check = assertTransition(state, ['submitted'], action);
619
+ if (!check.ok) {
620
+ fail(check.reason);
621
+ } else {
622
+ const roleCheck = requireRoleForAction(state, options, action);
623
+ if (!roleCheck.ok) {
624
+ fail(roleCheck.reason);
625
+ } else {
626
+ toStatus = 'rejected';
627
+ state.status = toStatus;
628
+ state.approvals.status = 'rejected';
629
+ state.approvals.rejected_by = actor;
630
+ }
631
+ }
632
+ } else if (action === 'execute') {
633
+ const validFrom = ['submitted', 'approved'];
634
+ const check = assertTransition(state, validFrom, action);
635
+ if (!check.ok) {
636
+ fail(check.reason);
637
+ } else if (state.approval_required && state.status !== 'approved') {
638
+ fail('approval required before execute');
639
+ } else {
640
+ const roleCheck = requireRoleForAction(state, options, action);
641
+ if (!roleCheck.ok) {
642
+ fail(roleCheck.reason);
643
+ } else {
644
+ const auth = requirePasswordForAction(state, options, action, now);
645
+ if (!auth.ok) {
646
+ fail(auth.reason);
647
+ } else {
648
+ toStatus = 'executed';
649
+ state.status = toStatus;
650
+ }
651
+ }
652
+ }
653
+ } else if (action === 'verify') {
654
+ const check = assertTransition(state, ['executed'], action);
655
+ if (!check.ok) {
656
+ fail(check.reason);
657
+ } else {
658
+ const roleCheck = requireRoleForAction(state, options, action);
659
+ if (!roleCheck.ok) {
660
+ fail(roleCheck.reason);
661
+ } else {
662
+ toStatus = 'verified';
663
+ state.status = toStatus;
664
+ }
665
+ }
666
+ } else if (action === 'archive') {
667
+ const check = assertTransition(state, ['verified', 'rejected'], action);
668
+ if (!check.ok) {
669
+ fail(check.reason);
670
+ } else {
671
+ const roleCheck = requireRoleForAction(state, options, action);
672
+ if (!roleCheck.ok) {
673
+ fail(roleCheck.reason);
674
+ } else {
675
+ toStatus = 'archived';
676
+ state.status = toStatus;
677
+ }
678
+ }
679
+ } else {
680
+ fail(`unsupported action: ${action}`);
681
+ }
682
+
683
+ const event = createEvent(
684
+ state,
685
+ action,
686
+ actor,
687
+ options && options.actorRole ? options.actorRole : null,
688
+ comment,
689
+ fromStatus,
690
+ blocked ? fromStatus : toStatus,
691
+ blocked,
692
+ reason
693
+ );
694
+ state.history.push(event);
695
+ state.updated_at = now;
696
+ return { blocked, reason, event };
697
+ }
698
+
699
+ async function appendAuditLine(auditPath, event) {
700
+ await fs.ensureDir(path.dirname(auditPath));
701
+ await fs.appendFile(auditPath, `${JSON.stringify(event)}\n`, 'utf8');
702
+ }
703
+
704
+ function buildAuthorizationSummary(state) {
705
+ const authorization = state && state.authorization && typeof state.authorization === 'object'
706
+ ? state.authorization
707
+ : {};
708
+ return {
709
+ password_required: authorization.password_required === true,
710
+ password_scope: Array.isArray(authorization.password_scope) ? authorization.password_scope : [],
711
+ password_verified: authorization.password_verified === true,
712
+ password_verified_at: authorization.password_verified_at || null,
713
+ password_expires_at: authorization.password_expires_at || null,
714
+ password_hash_env: authorization.password_hash_env || DEFAULT_PASSWORD_HASH_ENV,
715
+ verifier_configured: Boolean(resolveVerifierHash(state, process.env)),
716
+ role_requirements: normalizeRoleRequirements(authorization.role_requirements || {})
717
+ };
718
+ }
719
+
720
+ function buildOutput(state, options, statePath, auditPath, decision, reason) {
721
+ const publicState = JSON.parse(JSON.stringify(state || {}));
722
+ if (
723
+ publicState.authorization &&
724
+ typeof publicState.authorization === 'object' &&
725
+ publicState.authorization.password_hash
726
+ ) {
727
+ publicState.authorization.password_hash = '***';
728
+ }
729
+ return {
730
+ mode: 'interactive-approval-workflow',
731
+ generated_at: new Date().toISOString(),
732
+ action: options.action,
733
+ actor: options.actor || null,
734
+ actor_role: options.actorRole || null,
735
+ decision,
736
+ reason: reason || null,
737
+ state: publicState,
738
+ authorization: buildAuthorizationSummary(state),
739
+ output: {
740
+ state: path.relative(process.cwd(), statePath) || '.',
741
+ audit: path.relative(process.cwd(), auditPath) || '.'
742
+ }
743
+ };
744
+ }
745
+
746
+ async function main() {
747
+ const options = parseArgs(process.argv.slice(2));
748
+ const cwd = process.cwd();
749
+ const statePath = resolvePath(cwd, options.stateFile);
750
+ const auditPath = resolvePath(cwd, options.auditFile);
751
+
752
+ if (options.action === 'init') {
753
+ if ((await fs.pathExists(statePath)) && !options.force) {
754
+ throw new Error(`state file already exists; use --force to re-init: ${statePath}`);
755
+ }
756
+ const planPath = resolvePath(cwd, options.plan);
757
+ const plan = await readJsonFile(planPath, 'plan');
758
+ const roleRequirements = await resolveRoleRequirements(cwd, options, plan);
759
+ const initOptions = {
760
+ ...options,
761
+ roleRequirements
762
+ };
763
+ const { state, event } = buildInitialState(plan, options.actor, options.comment, initOptions);
764
+ await fs.ensureDir(path.dirname(statePath));
765
+ await fs.writeJson(statePath, state, { spaces: 2 });
766
+ await appendAuditLine(auditPath, event);
767
+ const payload = buildOutput(state, options, statePath, auditPath, 'ok', null);
768
+ if (options.json) {
769
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
770
+ } else {
771
+ process.stdout.write(`Interactive approval workflow initialized: ${state.workflow_id}\n`);
772
+ process.stdout.write(`- State: ${payload.output.state}\n`);
773
+ process.stdout.write(`- Audit: ${payload.output.audit}\n`);
774
+ }
775
+ return;
776
+ }
777
+
778
+ if (!(await fs.pathExists(statePath))) {
779
+ throw new Error(`state file not found: ${statePath}`);
780
+ }
781
+ const state = await readJsonFile(statePath, 'state-file');
782
+
783
+ if (options.action === 'status') {
784
+ const payload = buildOutput(state, options, statePath, auditPath, 'ok', null);
785
+ if (options.json) {
786
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
787
+ } else {
788
+ process.stdout.write(`Interactive approval status: ${state.status}\n`);
789
+ process.stdout.write(`- State: ${payload.output.state}\n`);
790
+ }
791
+ return;
792
+ }
793
+
794
+ const result = mutateStateForAction(state, options);
795
+ await fs.ensureDir(path.dirname(statePath));
796
+ await fs.writeJson(statePath, state, { spaces: 2 });
797
+ await appendAuditLine(auditPath, result.event);
798
+ const payload = buildOutput(
799
+ state,
800
+ options,
801
+ statePath,
802
+ auditPath,
803
+ result.blocked ? 'blocked' : 'ok',
804
+ result.reason
805
+ );
806
+
807
+ if (options.json) {
808
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
809
+ } else {
810
+ process.stdout.write(`Interactive approval action ${options.action}: ${payload.decision}\n`);
811
+ process.stdout.write(`- Status: ${state.status}\n`);
812
+ if (payload.reason) {
813
+ process.stdout.write(`- Reason: ${payload.reason}\n`);
814
+ }
815
+ process.stdout.write(`- Password required: ${payload.authorization.password_required ? 'yes' : 'no'}\n`);
816
+ process.stdout.write(`- Password verified: ${payload.authorization.password_verified ? 'yes' : 'no'}\n`);
817
+ process.stdout.write(`- State: ${payload.output.state}\n`);
818
+ process.stdout.write(`- Audit: ${payload.output.audit}\n`);
819
+ }
820
+
821
+ if (result.blocked) {
822
+ process.exitCode = 2;
823
+ }
824
+ }
825
+
826
+ main().catch((error) => {
827
+ console.error(`Interactive approval workflow failed: ${error.message}`);
828
+ process.exit(1);
829
+ });