@principles/pd-cli 1.138.0 → 1.140.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js +3 -3
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +15 -3
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +160 -49
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-artifact-repair.d.ts +58 -0
- package/dist/commands/runtime-artifact-repair.d.ts.map +1 -0
- package/dist/commands/runtime-artifact-repair.js +415 -0
- package/dist/commands/runtime-artifact-repair.js.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/runtime-activation-nextaction.test.ts +5 -3
- package/src/commands/runtime-activation.ts +161 -59
- package/src/commands/runtime-artifact-repair.ts +524 -0
- package/src/index.ts +21 -0
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +26 -0
- package/tests/commands/runtime-activation.test.ts +68 -7
- package/tests/commands/runtime-artifact-repair-registration.test.ts +74 -0
- package/tests/commands/runtime-artifact-repair.test.ts +404 -0
- package/tests/e2e/cross-package-acceptance.test.ts +3 -5
|
@@ -34,15 +34,17 @@ describe('deriveActivationStatusAndNextAction', () => {
|
|
|
34
34
|
expect(nextAction).not.toContain('--confirm');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it('shadow mode:
|
|
37
|
+
it('shadow mode: nextAction requires Owner review instead of advertising direct mutation', () => {
|
|
38
38
|
const { status, nextAction } = deriveActivationStatusAndNextAction({
|
|
39
39
|
...base,
|
|
40
40
|
mode: 'shadow',
|
|
41
41
|
});
|
|
42
42
|
expect(status).toBe('active');
|
|
43
43
|
expect(nextAction).toBeDefined();
|
|
44
|
-
expect(nextAction).
|
|
45
|
-
|
|
44
|
+
expect(nextAction).toBe(
|
|
45
|
+
'Keep shadow; promotion requires an authenticated Owner decision, immutable evidence bindings, and a passing Promotion Readiness result.',
|
|
46
|
+
);
|
|
47
|
+
expect(nextAction).not.toContain('pd activation promote');
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
it('deactivated record: status deactivated, no nextAction', () => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
4
|
import type { Command } from 'commander';
|
|
3
5
|
import {
|
|
4
6
|
RuntimeStateManager,
|
|
@@ -15,6 +17,12 @@ import {
|
|
|
15
17
|
isArtifactRevisionOf,
|
|
16
18
|
extractEvidenceRefs,
|
|
17
19
|
extractPrincipleId,
|
|
20
|
+
isFeatureEnabled,
|
|
21
|
+
RuleCodeOwnerDecisionService,
|
|
22
|
+
PromotionReadinessReader,
|
|
23
|
+
SqliteActivationSafetyStore,
|
|
24
|
+
collectOpenClawPromotionChecks,
|
|
25
|
+
summarizeRuleCodeShadowEvents,
|
|
18
26
|
} from '@principles/core/runtime-v2';
|
|
19
27
|
import type {
|
|
20
28
|
ActivationDecision,
|
|
@@ -23,7 +31,8 @@ import type {
|
|
|
23
31
|
ApprovalDecisionResult,
|
|
24
32
|
ApprovalCompletionResult,
|
|
25
33
|
} from '@principles/core/runtime-v2';
|
|
26
|
-
import type { PIArtifactRecord, ActivationStatusRecord } from '@principles/core/runtime-v2';
|
|
34
|
+
import type { PIArtifactRecord, ActivationStatusRecord, PromotionEvidenceSnapshot } from '@principles/core/runtime-v2';
|
|
35
|
+
import { OPENCLAW_HOST_LIVENESS_CONTRACT } from '@principles/host-runtime';
|
|
27
36
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
28
37
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
|
|
29
38
|
|
|
@@ -35,6 +44,22 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
35
44
|
return value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value);
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
function unavailableShadowSummary(): PromotionEvidenceSnapshot['shadowSummary'] {
|
|
48
|
+
return {
|
|
49
|
+
observed: null, matched: null, wouldBlock: null, wouldAllow: null,
|
|
50
|
+
requireApproval: null, autoCorrect: null, errors: null, neutralControl: null,
|
|
51
|
+
firstObservedAt: null, lastObservedAt: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readShadowSummary(workspaceDir: string, activationId: string): PromotionEvidenceSnapshot['shadowSummary'] {
|
|
56
|
+
const logsDir = path.join(workspaceDir, '.pd', 'logs'); if (!fs.existsSync(logsDir)) return unavailableShadowSummary();
|
|
57
|
+
const entries: unknown[] = [];
|
|
58
|
+
try { for (const file of fs.readdirSync(logsDir).filter(name => /^events_.*\.jsonl$/.test(name)).sort().slice(-7)) for (const line of fs.readFileSync(path.join(logsDir, file), 'utf8').split('\n').filter(Boolean)) { try { entries.push(JSON.parse(line) as unknown); } catch { /* exclude malformed telemetry */ } } }
|
|
59
|
+
catch { return unavailableShadowSummary(); }
|
|
60
|
+
return summarizeRuleCodeShadowEvents(entries, activationId);
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
interface ActivationDispatchOptions {
|
|
39
64
|
workspace?: string;
|
|
40
65
|
artifactId?: string;
|
|
@@ -345,6 +370,12 @@ export interface ActivationPromoteOptions {
|
|
|
345
370
|
dryRun?: boolean;
|
|
346
371
|
confirm?: boolean;
|
|
347
372
|
json?: boolean;
|
|
373
|
+
artifactId?: string;
|
|
374
|
+
artifactDigest?: string;
|
|
375
|
+
controlVersion?: number;
|
|
376
|
+
idempotencyKey?: string;
|
|
377
|
+
reasonCode?: string;
|
|
378
|
+
note?: string;
|
|
348
379
|
}
|
|
349
380
|
|
|
350
381
|
export interface ActivationPromoteResult {
|
|
@@ -353,13 +384,19 @@ export interface ActivationPromoteResult {
|
|
|
353
384
|
activationId: string;
|
|
354
385
|
promotedAt?: string;
|
|
355
386
|
reason?: string;
|
|
387
|
+
reasonCode?: string;
|
|
388
|
+
summary?: string;
|
|
389
|
+
failedChecks?: { checkId: string; reasonCode: string }[];
|
|
356
390
|
nextAction?: string;
|
|
357
391
|
}
|
|
358
392
|
|
|
359
393
|
export async function handleRuntimeActivationPromote(opts: ActivationPromoteOptions): Promise<void> {
|
|
360
394
|
const activationId = opts.activationId?.trim() ?? '';
|
|
361
|
-
const refuse = (reason: string, nextAction: string): void => {
|
|
362
|
-
const result: ActivationPromoteResult = {
|
|
395
|
+
const refuse = (reason: string, nextAction: string, details?: { summary?: string; failedChecks?: { checkId: string; reasonCode: string }[] }): void => {
|
|
396
|
+
const result: ActivationPromoteResult = {
|
|
397
|
+
ok: false, decision: 'refused', activationId, reason, reasonCode: reason, nextAction,
|
|
398
|
+
summary: details?.summary, failedChecks: details?.failedChecks,
|
|
399
|
+
};
|
|
363
400
|
if (opts.json) console.log(JSON.stringify(result));
|
|
364
401
|
else {
|
|
365
402
|
console.error(`Promotion refused: ${reason}`);
|
|
@@ -380,63 +417,116 @@ export async function handleRuntimeActivationPromote(opts: ActivationPromoteOpti
|
|
|
380
417
|
let stateManager: RuntimeStateManager | undefined;
|
|
381
418
|
try {
|
|
382
419
|
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
if (matchingShadows.length > 1) {
|
|
406
|
-
refuse(
|
|
407
|
-
'duplicate_shadow_activations',
|
|
408
|
-
`${matchingShadows.length} shadow activations share activation_id=${activationId}; resolve duplicates before promoting.`,
|
|
409
|
-
);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (opts.confirm !== true) {
|
|
414
|
-
const result: ActivationPromoteResult = {
|
|
415
|
-
ok: true,
|
|
416
|
-
decision: 'would_promote',
|
|
417
|
-
activationId,
|
|
418
|
-
nextAction: `Run pd activation promote --activation-id ${activationId} --confirm to enable live blocking.`,
|
|
419
|
-
};
|
|
420
|
-
if (opts.json) console.log(JSON.stringify(result));
|
|
421
|
-
else {
|
|
422
|
-
console.log(`Would promote: ${activationId}`);
|
|
423
|
-
console.log(` nextAction: ${result.nextAction}`);
|
|
420
|
+
const flags = computeFlagsFromLoadResult(loadPdConfig(workspaceDir));
|
|
421
|
+
const ownerId = process.env.PD_OWNER_ID?.trim();
|
|
422
|
+
const credentialId = process.env.PD_OWNER_CREDENTIAL_ID?.trim();
|
|
423
|
+
const consoleToken = process.env.PD_CONSOLE_TOKEN?.trim();
|
|
424
|
+
const operatorId = process.env.USERNAME?.trim() || process.env.USER?.trim();
|
|
425
|
+
const actor = ownerId && credentialId && consoleToken
|
|
426
|
+
? {
|
|
427
|
+
principal: { kind: 'configured_owner' as const, ownerId },
|
|
428
|
+
authentication: { method: 'cli_owner_credential' as const, credentialId },
|
|
429
|
+
...(operatorId ? { operator: { kind: 'local_user' as const, operatorId } } : {}),
|
|
430
|
+
}
|
|
431
|
+
: {
|
|
432
|
+
principal: { kind: 'break_glass' as const, reason: 'local_no_auth_emergency' as const },
|
|
433
|
+
authentication: { method: 'local_break_glass' as const },
|
|
434
|
+
};
|
|
435
|
+
const getStateManager = async (): Promise<RuntimeStateManager> => {
|
|
436
|
+
if (!stateManager) {
|
|
437
|
+
stateManager = new RuntimeStateManager({ workspaceDir, readonly: opts.dryRun === true });
|
|
438
|
+
await stateManager.initialize();
|
|
424
439
|
}
|
|
440
|
+
return stateManager;
|
|
441
|
+
};
|
|
442
|
+
const service = new RuleCodeOwnerDecisionService({
|
|
443
|
+
ownerLiveDecisionEnabled: () => isFeatureEnabled(flags, 'rulecode_owner_live_decision'),
|
|
444
|
+
safetyControlsEnabled: () => isFeatureEnabled(flags, 'rulecode_safety_controls'),
|
|
445
|
+
evaluateReadiness: async request => {
|
|
446
|
+
const manager = await getStateManager();
|
|
447
|
+
const activationStore = new SqliteActivationStateStore(manager.connection);
|
|
448
|
+
const writer = new RuleHostWriter({
|
|
449
|
+
gateDeps: createProductionGateDeps(),
|
|
450
|
+
featureFlagProbe: flagId => isFeatureEnabled(flags, flagId),
|
|
451
|
+
});
|
|
452
|
+
const reader = new PromotionReadinessReader({
|
|
453
|
+
listCodeToolHookActivations: () => activationStore.listCodeToolHookActivations(false),
|
|
454
|
+
getArtifactById: artifactId => manager.piArtifactStore.getArtifactById(artifactId),
|
|
455
|
+
computeArtifactDigest: artifact => `sha256:${createHash('sha256').update(JSON.stringify(artifact), 'utf8').digest('hex')}`,
|
|
456
|
+
validateProductionArtifact: artifact => writer.canActivate(artifact),
|
|
457
|
+
collectHostChecks: async artifact => {
|
|
458
|
+
const liveArtifacts: PIArtifactSnapshot[] = [];
|
|
459
|
+
const activations = await activationStore.listCodeToolHookActivations(false);
|
|
460
|
+
for (const active of activations) {
|
|
461
|
+
if (active.action !== 'code_tool_hook_live_activate' || active.deactivatedAt !== null) continue;
|
|
462
|
+
const liveArtifact = await manager.piArtifactStore.getArtifactById(active.artifactId);
|
|
463
|
+
if (liveArtifact) liveArtifacts.push(liveArtifact);
|
|
464
|
+
}
|
|
465
|
+
return collectOpenClawPromotionChecks(artifact, {
|
|
466
|
+
ownerIdentityConfigured: actor.principal.kind === 'configured_owner'
|
|
467
|
+
&& actor.authentication.method === 'cli_owner_credential',
|
|
468
|
+
safetyControlsEnabled: isFeatureEnabled(flags, 'rulecode_safety_controls'),
|
|
469
|
+
hostContract: OPENCLAW_HOST_LIVENESS_CONTRACT,
|
|
470
|
+
existingLiveArtifacts: liveArtifacts,
|
|
471
|
+
validateProductionArtifact: value => writer.canActivate(value),
|
|
472
|
+
});
|
|
473
|
+
},
|
|
474
|
+
buildEvidenceSnapshot: (checks, artifact) => {
|
|
475
|
+
const createdAt = new Date().toISOString();
|
|
476
|
+
const artifactDigest = artifact
|
|
477
|
+
? `sha256:${createHash('sha256').update(JSON.stringify(artifact), 'utf8').digest('hex')}`
|
|
478
|
+
: request.expectedArtifactDigest;
|
|
479
|
+
const snapshotBody = JSON.stringify({ artifactDigest, checks, createdAt });
|
|
480
|
+
return {
|
|
481
|
+
snapshotId: `snapshot-${randomUUID()}`,
|
|
482
|
+
snapshotDigest: `sha256:${createHash('sha256').update(snapshotBody, 'utf8').digest('hex')}`,
|
|
483
|
+
artifactDigest,
|
|
484
|
+
lineageRefs: artifact ? [artifact.sourceTaskId, ...artifact.lineageArtifactIds] : [],
|
|
485
|
+
hostRuntimeVersion: 'openclaw-legacy@1', safetyGateResults: checks,
|
|
486
|
+
shadowSummary: readShadowSummary(workspaceDir, activationId),
|
|
487
|
+
configurationVersion: 'pd-config-current',
|
|
488
|
+
redaction: { version: 'v1', rawParametersStored: false }, createdAt,
|
|
489
|
+
};
|
|
490
|
+
},
|
|
491
|
+
newEvaluationId: () => `readiness-${randomUUID()}`,
|
|
492
|
+
});
|
|
493
|
+
return reader.evaluate(request);
|
|
494
|
+
},
|
|
495
|
+
commitPromotion: async input => {
|
|
496
|
+
const manager = await getStateManager();
|
|
497
|
+
return new SqliteActivationSafetyStore(manager.connection).commitPromotion(input);
|
|
498
|
+
},
|
|
499
|
+
newDecisionId: () => `decision-${randomUUID()}`,
|
|
500
|
+
now: () => new Date().toISOString(),
|
|
501
|
+
});
|
|
502
|
+
const result = await service.promote({
|
|
503
|
+
activationId,
|
|
504
|
+
expectedArtifactId: opts.artifactId?.trim() ?? '',
|
|
505
|
+
expectedArtifactDigest: opts.artifactDigest?.trim() ?? '',
|
|
506
|
+
expectedControlVersion: opts.controlVersion ?? 0,
|
|
507
|
+
idempotencyKey: opts.idempotencyKey?.trim() ?? '',
|
|
508
|
+
reasonCode: opts.reasonCode?.trim() ?? '',
|
|
509
|
+
note: opts.note,
|
|
510
|
+
confirmed: opts.confirm === true,
|
|
511
|
+
dryRun: opts.dryRun === true,
|
|
512
|
+
}, actor);
|
|
513
|
+
if (!result.ok) {
|
|
514
|
+
refuse(result.reasonCode, result.nextAction, { summary: result.summary, failedChecks: result.failedChecks });
|
|
425
515
|
return;
|
|
426
516
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
517
|
+
if (result.decision === 'would_promote') {
|
|
518
|
+
const output: ActivationPromoteResult = {
|
|
519
|
+
ok: true, decision: 'would_promote', activationId: result.activationId,
|
|
520
|
+
summary: `Readiness ${result.readinessEvaluationId} passed without mutation.`,
|
|
521
|
+
nextAction: `Re-run with --confirm using evidence snapshot ${result.evidenceSnapshotDigest}.`,
|
|
522
|
+
};
|
|
523
|
+
if (opts.json) console.log(JSON.stringify(output));
|
|
524
|
+
else console.log(`Would promote: ${result.activationId}`);
|
|
432
525
|
return;
|
|
433
526
|
}
|
|
434
|
-
const
|
|
435
|
-
if (opts.json) console.log(JSON.stringify(
|
|
436
|
-
else {
|
|
437
|
-
console.log(`Promoted live: ${activationId}`);
|
|
438
|
-
console.log(` promotedAt: ${promotedAt}`);
|
|
439
|
-
}
|
|
527
|
+
const output: ActivationPromoteResult = { ok: true, decision: 'promoted', activationId: result.activationId, promotedAt: result.promotedAt };
|
|
528
|
+
if (opts.json) console.log(JSON.stringify(output));
|
|
529
|
+
else console.log(`Promoted live: ${result.activationId}`);
|
|
440
530
|
} catch (err: unknown) {
|
|
441
531
|
refuse(
|
|
442
532
|
`promotion_failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -472,9 +562,9 @@ export interface ActivationStatusDerivation {
|
|
|
472
562
|
/**
|
|
473
563
|
* Derive owner-facing status + nextAction for an activation record.
|
|
474
564
|
*
|
|
475
|
-
* Extract as a pure function so the CLI hints
|
|
476
|
-
*
|
|
477
|
-
*
|
|
565
|
+
* Extract as a pure function so the CLI hints can be regression-tested.
|
|
566
|
+
* Shadow activations point at the Owner decision contract instead of
|
|
567
|
+
* advertising a raw lifecycle mutation that can bypass review evidence.
|
|
478
568
|
*/
|
|
479
569
|
export function deriveActivationStatusAndNextAction(
|
|
480
570
|
input: ActivationNextActionInput,
|
|
@@ -492,7 +582,7 @@ export function deriveActivationStatusAndNextAction(
|
|
|
492
582
|
if (mode === 'shadow') {
|
|
493
583
|
return {
|
|
494
584
|
status: 'active',
|
|
495
|
-
nextAction:
|
|
585
|
+
nextAction: 'Keep shadow; promotion requires an authenticated Owner decision, immutable evidence bindings, and a passing Promotion Readiness result.',
|
|
496
586
|
};
|
|
497
587
|
}
|
|
498
588
|
if (mode === 'live') {
|
|
@@ -1257,6 +1347,12 @@ export function registerRuntimeActivationPromoteCommand(parent: Command): Comman
|
|
|
1257
1347
|
.option('-w, --workspace <path>', 'Workspace directory')
|
|
1258
1348
|
.option('--dry-run', 'Validate eligibility without changing activation state')
|
|
1259
1349
|
.option('--confirm', 'Confirm promotion to live blocking')
|
|
1350
|
+
.option('--artifact-id <id>', 'Expected artifact ID from Owner review')
|
|
1351
|
+
.option('--artifact-digest <digest>', 'Expected artifact digest from Owner review')
|
|
1352
|
+
.option('--control-version <n>', 'Expected activation control version', (value) => Number.parseInt(value, 10))
|
|
1353
|
+
.option('--idempotency-key <key>', 'Idempotency key for the Owner decision')
|
|
1354
|
+
.option('--reason <code>', 'Owner decision reason code')
|
|
1355
|
+
.option('--note <text>', 'Required CLI Owner review note')
|
|
1260
1356
|
.option('--json', 'Output raw JSON')
|
|
1261
1357
|
.action(async (opts) => {
|
|
1262
1358
|
await handleRuntimeActivationPromote({
|
|
@@ -1265,6 +1361,12 @@ export function registerRuntimeActivationPromoteCommand(parent: Command): Comman
|
|
|
1265
1361
|
dryRun: opts.dryRun,
|
|
1266
1362
|
confirm: opts.confirm,
|
|
1267
1363
|
json: opts.json,
|
|
1364
|
+
artifactId: opts.artifactId,
|
|
1365
|
+
artifactDigest: opts.artifactDigest,
|
|
1366
|
+
controlVersion: opts.controlVersion,
|
|
1367
|
+
idempotencyKey: opts.idempotencyKey,
|
|
1368
|
+
reasonCode: opts.reason,
|
|
1369
|
+
note: opts.note,
|
|
1268
1370
|
});
|
|
1269
1371
|
});
|
|
1270
1372
|
}
|