@principles/pd-cli 1.151.1 → 1.152.1
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/console.d.ts.map +1 -1
- package/dist/commands/console.js +15 -3
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +50 -21
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/services/console-launcher.d.ts +9 -0
- package/dist/services/console-launcher.d.ts.map +1 -1
- package/dist/services/console-launcher.js +47 -4
- package/dist/services/console-launcher.js.map +1 -1
- package/dist/services/pd-config-loader.d.ts +5 -0
- package/dist/services/pd-config-loader.d.ts.map +1 -1
- package/dist/services/pd-config-loader.js +26 -1
- package/dist/services/pd-config-loader.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +5 -3
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +10 -1
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/console.ts +16 -3
- package/src/commands/runtime-internalization-run-rulehost.ts +51 -21
- package/src/services/console-launcher.ts +57 -5
- package/src/services/pd-config-loader.ts +27 -1
- package/src/services/rulehost-pipeline-runner.ts +15 -4
- package/tests/commands/console-open.test.ts +59 -1
- package/tests/commands/run-rulehost-handler.test.ts +53 -3
- package/tests/commands/runtime-activation.test.ts +3 -1
- package/tests/e2e/cross-package-acceptance.test.ts +26 -3
- package/tests/services/rulehost-pipeline-e2e.test.ts +20 -2
- package/tests/services/rulehost-pipeline-runner.test.ts +28 -4
|
@@ -328,7 +328,9 @@ describe('planConsoleLaunch — reused (healthy console on preferred port)', ()
|
|
|
328
328
|
expect(result.reason).toBe('console_authentication_mode_mismatch');
|
|
329
329
|
// PRI-695: the guidance names the actual running state and both ways
|
|
330
330
|
// out — it no longer references "Companion" (meaningless to CLI users)
|
|
331
|
-
// or PD_CONSOLE_TOKEN (never set by the default install).
|
|
331
|
+
// or PD_CONSOLE_TOKEN (never set by the default install). PRI-784: the
|
|
332
|
+
// no-source form stays source-neutral; the env/flag variants have
|
|
333
|
+
// their own cases below.
|
|
332
334
|
expect(result.nextAction).toContain('without authentication');
|
|
333
335
|
expect(result.nextAction).toContain(`port ${addr.port}`);
|
|
334
336
|
expect(result.nextAction).not.toContain('Companion');
|
|
@@ -338,6 +340,56 @@ describe('planConsoleLaunch — reused (healthy console on preferred port)', ()
|
|
|
338
340
|
}
|
|
339
341
|
});
|
|
340
342
|
|
|
343
|
+
it('auth-mismatch guidance names PD_CONSOLE_TOKEN and its durable removal when the token came from the env (PRI-784)', async () => {
|
|
344
|
+
const server = http.createServer((_req, res) => {
|
|
345
|
+
res.statusCode = 200;
|
|
346
|
+
res.end(JSON.stringify({ success: true, data: { authenticationMode: 'no_auth' } }));
|
|
347
|
+
});
|
|
348
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
349
|
+
const addr = server.address();
|
|
350
|
+
if (typeof addr !== 'object' || !addr) throw new Error('no addr');
|
|
351
|
+
try {
|
|
352
|
+
const result = await planConsoleLaunch({
|
|
353
|
+
workspaceDir: '/tmp/anywhere', preferredPort: addr.port,
|
|
354
|
+
host: '127.0.0.1', token: 'configured-token', tokenSource: 'env',
|
|
355
|
+
});
|
|
356
|
+
expect(result.status).toBe('refused');
|
|
357
|
+
expect(result.reason).toBe('console_authentication_mode_mismatch');
|
|
358
|
+
// The env source is the recurring real-world trap: a persistently set
|
|
359
|
+
// variable makes "reopen without a token" impossible to follow, so the
|
|
360
|
+
// guidance must name the variable and how to remove it at its source.
|
|
361
|
+
expect(result.nextAction).toContain('PD_CONSOLE_TOKEN');
|
|
362
|
+
expect(result.nextAction).toContain("SetEnvironmentVariable('PD_CONSOLE_TOKEN', $null, 'User')");
|
|
363
|
+
expect(result.nextAction).toContain('--no-auth');
|
|
364
|
+
expect(result.nextAction).toContain('--token');
|
|
365
|
+
} finally {
|
|
366
|
+
await new Promise<void>((resolve) => server.close(resolve));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('auth-mismatch guidance names --token, never the env var, when the token came from the flag (PRI-784)', async () => {
|
|
371
|
+
const server = http.createServer((_req, res) => {
|
|
372
|
+
res.statusCode = 200;
|
|
373
|
+
res.end(JSON.stringify({ success: true, data: { authenticationMode: 'no_auth' } }));
|
|
374
|
+
});
|
|
375
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
376
|
+
const addr = server.address();
|
|
377
|
+
if (typeof addr !== 'object' || !addr) throw new Error('no addr');
|
|
378
|
+
try {
|
|
379
|
+
const result = await planConsoleLaunch({
|
|
380
|
+
workspaceDir: '/tmp/anywhere', preferredPort: addr.port,
|
|
381
|
+
host: '127.0.0.1', token: 'configured-token', tokenSource: 'flag',
|
|
382
|
+
});
|
|
383
|
+
expect(result.status).toBe('refused');
|
|
384
|
+
expect(result.reason).toBe('console_authentication_mode_mismatch');
|
|
385
|
+
expect(result.nextAction).toContain('--token');
|
|
386
|
+
expect(result.nextAction).toContain('--no-auth');
|
|
387
|
+
expect(result.nextAction).not.toContain('PD_CONSOLE_TOKEN');
|
|
388
|
+
} finally {
|
|
389
|
+
await new Promise<void>((resolve) => server.close(resolve));
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
341
393
|
it('reuses a no-auth Console for a tokenless caller (PRI-695 default post-install state)', async () => {
|
|
342
394
|
// The installer auto-launches the Console with --no-auth; the documented
|
|
343
395
|
// reopen command carries no flags and no token. That combination must
|
|
@@ -724,6 +776,12 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
724
776
|
expect(run.parsed.status).toBe('refused');
|
|
725
777
|
expect(run.parsed.reason).toBe('console_authentication_mode_mismatch');
|
|
726
778
|
expect(run.parsed).not.toHaveProperty('serverPid');
|
|
779
|
+
// PRI-784 review fix (PR #1670): the fresh-start env guidance is part of
|
|
780
|
+
// the user contract — assert its content, not just the refusal shell.
|
|
781
|
+
expect(run.parsed.nextAction).toContain('PD_CONSOLE_TOKEN');
|
|
782
|
+
expect(run.parsed.nextAction).toContain('not forwarded');
|
|
783
|
+
expect(run.parsed.nextAction).toContain('--token "$PD_CONSOLE_TOKEN"');
|
|
784
|
+
expect(run.parsed.nextAction).toContain('--no-auth');
|
|
727
785
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
728
786
|
expect(await isPortInUse('127.0.0.1', 49391)).toBe(false);
|
|
729
787
|
} finally {
|
|
@@ -573,6 +573,37 @@ function writeContextV2EnabledConfig(workspaceDir: string): void {
|
|
|
573
573
|
fs.writeFileSync(path.join(configDir, 'config.yaml'), yaml.dump(cfg), 'utf8');
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
function writeContextV2KillSwitchConfig(workspaceDir: string): void {
|
|
577
|
+
const configDir = path.join(workspaceDir, '.pd');
|
|
578
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
579
|
+
const cfg = {
|
|
580
|
+
version: 1,
|
|
581
|
+
features: {
|
|
582
|
+
prompt: { category: 'core', enabled: true },
|
|
583
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
584
|
+
defer_archive: { category: 'core', enabled: true },
|
|
585
|
+
code_rule_capability: { category: 'core', enabled: true },
|
|
586
|
+
// PRI-780: explicit kill switch (the flag itself defaults ON).
|
|
587
|
+
rulecode_context_v2: { category: 'quiet', enabled: false },
|
|
588
|
+
},
|
|
589
|
+
workspace: { default: workspaceDir },
|
|
590
|
+
runtimeProfiles: {
|
|
591
|
+
'pi-ai.default': { type: 'pi-ai', provider: 'anthropic', model: 'claude-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
592
|
+
},
|
|
593
|
+
internalAgents: {
|
|
594
|
+
defaultRuntime: 'pi-ai.default',
|
|
595
|
+
agents: {
|
|
596
|
+
dreamer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
597
|
+
philosopher: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
598
|
+
scribe: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
599
|
+
artificer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
600
|
+
evaluator: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
fs.writeFileSync(path.join(configDir, 'config.yaml'), yaml.dump(cfg), 'utf8');
|
|
605
|
+
}
|
|
606
|
+
|
|
576
607
|
describe('handleRunRuleHost — PR #1122 behavior-examples fail-fast (CodeRabbit Comment 1+2)', () => {
|
|
577
608
|
let workspaceDir: string;
|
|
578
609
|
let savedEnv: NodeJS.ProcessEnv;
|
|
@@ -648,8 +679,9 @@ describe('handleRunRuleHost — PR #1122 behavior-examples fail-fast (CodeRabbit
|
|
|
648
679
|
it('text dry-run output uses v2-aware capabilityStatus when --behavior-examples is missing (CodeRabbit Comment 2)', async () => {
|
|
649
680
|
// With contextV2Enabled=true and no --behavior-examples, the handler sets
|
|
650
681
|
// behaviorExamplesReason='behavior_examples_missing'. The text dry-run
|
|
651
|
-
// branch must
|
|
652
|
-
//
|
|
682
|
+
// branch must derive capabilityStatus from the EFFECTIVE capability
|
|
683
|
+
// (PRI-780 review M2), surfacing the reason — not the stale
|
|
684
|
+
// resolvedRuntime.capabilityStatus ("ON").
|
|
653
685
|
writeContextV2EnabledConfig(workspaceDir);
|
|
654
686
|
const { stdout } = await captureStdio(() =>
|
|
655
687
|
handleRunRuleHost({
|
|
@@ -658,6 +690,24 @@ describe('handleRunRuleHost — PR #1122 behavior-examples fail-fast (CodeRabbit
|
|
|
658
690
|
workspace: workspaceDir,
|
|
659
691
|
}),
|
|
660
692
|
);
|
|
661
|
-
expect(stdout).toMatch(/code_rule_capability: OFF \(behavior_examples_missing
|
|
693
|
+
expect(stdout).toMatch(/code_rule_capability: OFF \(behavior_examples_missing;/);
|
|
694
|
+
expect(stdout).not.toMatch(/code_rule_capability: ON/);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('PRI-780 M2: kill-switch dry-run reports OFF with the kill-switch reason, never ON', async () => {
|
|
698
|
+
writeContextV2KillSwitchConfig(workspaceDir);
|
|
699
|
+
const { stdout } = await captureStdio(() =>
|
|
700
|
+
handleRunRuleHost({
|
|
701
|
+
painId: 'pain-1',
|
|
702
|
+
dryRun: true,
|
|
703
|
+
workspace: workspaceDir,
|
|
704
|
+
}),
|
|
705
|
+
);
|
|
706
|
+
expect(stdout).toMatch(/code_rule_capability: OFF \(rulecode_context_v2_disabled/);
|
|
707
|
+
expect(stdout).not.toMatch(/code_rule_capability: ON/);
|
|
708
|
+
// Codex review round 2 P2: the plain-text "Next" line must derive from the
|
|
709
|
+
// EFFECTIVE capability too — confirmation cannot run the code-rule pipeline.
|
|
710
|
+
expect(stdout).not.toMatch(/Next: pass --confirm to actually run the pipeline/);
|
|
711
|
+
expect(stdout).toMatch(/Next: fix the code-rule capability issue above/);
|
|
662
712
|
});
|
|
663
713
|
});
|
|
@@ -657,7 +657,9 @@ describe('handleRuntimeActivationList', () => {
|
|
|
657
657
|
},
|
|
658
658
|
]);
|
|
659
659
|
// Default mockFeatureFlags has rulecode_context_v2 enabled. Flip it off
|
|
660
|
-
// to simulate the
|
|
660
|
+
// to simulate the migration-period kill switch (explicit
|
|
661
|
+
// features.rulecode_context_v2.enabled: false; the registry default is ON
|
|
662
|
+
// since PRI-780).
|
|
661
663
|
mockFeatureFlags.flags.rulecode_context_v2.enabled = false;
|
|
662
664
|
|
|
663
665
|
await handleRuntimeActivationList({
|
|
@@ -171,15 +171,32 @@ function scribeOut(taskId: string, priorId?: string): unknown {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// PRI-780: v2-only generation — the acceptance chain carries an Owner-labelled
|
|
175
|
+
// pack whose cases match artificerV2's goldenTraceCases (parity is checked).
|
|
176
|
+
const XPKG_RULE_CONTEXT = {
|
|
177
|
+
version: 2 as const,
|
|
178
|
+
history: { status: 'available' as const, truncated: false, calls: [] },
|
|
179
|
+
facts: { priorReadOfTarget: 'unknown' as const, readCount: 0, writeCount: 0, uniqueWritePathCount: 0, sameActionBlockCount: null },
|
|
180
|
+
};
|
|
181
|
+
const XPKG_TEST_PACK = {
|
|
182
|
+
sourceNegativeCase: { caseId: 'neg-1', kind: 'negative' as const, toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' as const, ruleContext: XPKG_RULE_CONTEXT },
|
|
183
|
+
ownerDesiredOutcome: 'block system path writes',
|
|
184
|
+
positiveCounterexamples: [{ caseId: 'pos-1', kind: 'positive' as const, toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' as const, ruleContext: XPKG_RULE_CONTEXT }],
|
|
185
|
+
evidenceRefs: ['pain://xpkg-1'],
|
|
186
|
+
redactionNotes: [],
|
|
187
|
+
};
|
|
188
|
+
|
|
174
189
|
function artificerV2(taskId: string, priorId?: string): unknown {
|
|
175
190
|
return {
|
|
176
191
|
taskId, sourceScribeArtifactId: requireLineage(priorId, 'sourceScribeArtifactId'),
|
|
192
|
+
requiresContextVersion: 2,
|
|
193
|
+
evidenceRefs: ['pain://xpkg-1'],
|
|
177
194
|
implementationPlan: { summary: 'Block /etc writes', targetSurface: 'rule-host', changes: ['matcher'], tests: ['unit'], rolloutNotes: ['shadow'], confidence: 0.85 },
|
|
178
195
|
implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); if (p.startsWith("/etc")) return { decision: "block", matched: true, reason: "system path" }; const ctx = input?.context; if (ctx && ctx.facts && ctx.facts.priorReadOfTarget === "no") { return { decision: "block", matched: true, reason: "no prior read" }; } return { decision: "allow", matched: false, reason: "ok" }; }',
|
|
179
196
|
implementationSummary: 'Block system path writes',
|
|
180
197
|
goldenTraceCases: [
|
|
181
|
-
{ caseId: 'pos-1', kind: 'positive', toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' },
|
|
182
|
-
{ caseId: 'neg-1', kind: 'negative', toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' },
|
|
198
|
+
{ caseId: 'pos-1', kind: 'positive', toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow', ruleContext: XPKG_RULE_CONTEXT },
|
|
199
|
+
{ caseId: 'neg-1', kind: 'negative', toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block', ruleContext: XPKG_RULE_CONTEXT },
|
|
183
200
|
],
|
|
184
201
|
affectedTools: ['write_file'],
|
|
185
202
|
sourceTrace: { scribeArtifactId: requireLineage(priorId, 'sourceTrace.scribeArtifactId') },
|
|
@@ -267,6 +284,7 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
|
|
|
267
284
|
pollIntervalMs: 5,
|
|
268
285
|
timeoutMs: 5000,
|
|
269
286
|
codeRuleCapability: capability,
|
|
287
|
+
behaviorExamplePack: XPKG_TEST_PACK,
|
|
270
288
|
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
271
289
|
});
|
|
272
290
|
|
|
@@ -377,7 +395,9 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
|
|
|
377
395
|
{
|
|
378
396
|
writers: [
|
|
379
397
|
new PromptWriter(),
|
|
380
|
-
|
|
398
|
+
// PRI-780: rulecode_context_v2 defaults ON — production callers
|
|
399
|
+
// inject the real flag probe; mirror that here for the v2 artifact.
|
|
400
|
+
new RuleHostWriter({ gateDeps: createProductionGateDeps(), featureFlagProbe: (flagId) => flagId === 'rulecode_context_v2' }),
|
|
381
401
|
new DeferArchiveWriter(),
|
|
382
402
|
],
|
|
383
403
|
approvalQueueStore: approvalStore,
|
|
@@ -426,12 +446,15 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
|
|
|
426
446
|
// Shadow mode is observation-only. The RuleHost loads the activation but
|
|
427
447
|
// records shadowDecisions instead of blocking. evaluate() must return
|
|
428
448
|
// undefined (no block) for shadow activations, even for /etc/passwd.
|
|
449
|
+
// PRI-780: production RuleHostInput always carries context (flag on) —
|
|
450
|
+
// the v2 activation only loads when context.version === 2.
|
|
429
451
|
const makeRuleHostInput = (targetPath: string): RuleHostInput => ({
|
|
430
452
|
action: { toolName: 'write_file', normalizedPath: targetPath, paramsSummary: { path: targetPath } },
|
|
431
453
|
workspace: { isRiskPath: targetPath.startsWith('/etc') },
|
|
432
454
|
session: { currentGfi: 0 },
|
|
433
455
|
evolution: { epTier: 0 },
|
|
434
456
|
derived: { estimatedLineChanges: 1, bashRisk: 'safe' },
|
|
457
|
+
context: XPKG_RULE_CONTEXT,
|
|
435
458
|
});
|
|
436
459
|
const shadowRuleHost = new RuleHost(
|
|
437
460
|
path.join(tmpDir, '.state'),
|
|
@@ -168,6 +168,15 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
168
168
|
const dir = mkTmpDir();
|
|
169
169
|
tmpDirs.push(dir);
|
|
170
170
|
writeConfig(dir, configFactory(dir));
|
|
171
|
+
// PRI-780: v2-only generation — every dry-run in this suite carries a
|
|
172
|
+
// valid Owner-labelled behavior-examples file so the capability assertions
|
|
173
|
+
// isolate their intended variable (agent/flag/key state), not the
|
|
174
|
+
// behavior_examples_missing refusal.
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
path.join(dir, 'behavior-examples.json'),
|
|
177
|
+
JSON.stringify({ ownerDesiredOutcome: 'block system path writes', sourceNegativeToolCallId: 1, positiveToolCallIds: [2] }),
|
|
178
|
+
'utf8',
|
|
179
|
+
);
|
|
171
180
|
return dir;
|
|
172
181
|
}
|
|
173
182
|
|
|
@@ -188,6 +197,7 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
188
197
|
await handleRunRuleHost({
|
|
189
198
|
workspace,
|
|
190
199
|
painId: 'pain-wiring-001',
|
|
200
|
+
behaviorExamples: 'behavior-examples.json',
|
|
191
201
|
dryRun: true,
|
|
192
202
|
json: true,
|
|
193
203
|
});
|
|
@@ -212,7 +222,7 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
212
222
|
return config;
|
|
213
223
|
});
|
|
214
224
|
|
|
215
|
-
await handleRunRuleHost({ workspace, painId: 'pain-flag-default', dryRun: true, json: true });
|
|
225
|
+
await handleRunRuleHost({ workspace, painId: 'pain-flag-default', behaviorExamples: 'behavior-examples.json', dryRun: true, json: true });
|
|
216
226
|
|
|
217
227
|
const output = parseJsonOutput();
|
|
218
228
|
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
@@ -235,7 +245,7 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
235
245
|
return config;
|
|
236
246
|
});
|
|
237
247
|
|
|
238
|
-
await handleRunRuleHost({ workspace, painId: 'pain-emergency-disable', dryRun: true, json: true });
|
|
248
|
+
await handleRunRuleHost({ workspace, painId: 'pain-emergency-disable', behaviorExamples: 'behavior-examples.json', dryRun: true, json: true });
|
|
239
249
|
|
|
240
250
|
const output = parseJsonOutput();
|
|
241
251
|
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
@@ -316,6 +326,7 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
316
326
|
await handleRunRuleHost({
|
|
317
327
|
workspace,
|
|
318
328
|
painId: 'pain-wiring-002',
|
|
329
|
+
behaviorExamples: 'behavior-examples.json',
|
|
319
330
|
dryRun: true,
|
|
320
331
|
json: true,
|
|
321
332
|
});
|
|
@@ -336,6 +347,7 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
336
347
|
await handleRunRuleHost({
|
|
337
348
|
workspace,
|
|
338
349
|
painId: 'pain-wiring-003',
|
|
350
|
+
behaviorExamples: 'behavior-examples.json',
|
|
339
351
|
dryRun: true,
|
|
340
352
|
json: true,
|
|
341
353
|
});
|
|
@@ -397,10 +409,16 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
397
409
|
},
|
|
398
410
|
},
|
|
399
411
|
});
|
|
412
|
+
fs.writeFileSync(
|
|
413
|
+
path.join(dir, 'behavior-examples.json'),
|
|
414
|
+
JSON.stringify({ ownerDesiredOutcome: 'block system path writes', sourceNegativeToolCallId: 1, positiveToolCallIds: [2] }),
|
|
415
|
+
'utf8',
|
|
416
|
+
);
|
|
400
417
|
|
|
401
418
|
await handleRunRuleHost({
|
|
402
419
|
workspace: dir,
|
|
403
420
|
painId: 'pain-wiring-004',
|
|
421
|
+
behaviorExamples: 'behavior-examples.json',
|
|
404
422
|
dryRun: true,
|
|
405
423
|
json: true,
|
|
406
424
|
});
|
|
@@ -134,16 +134,33 @@ function scribeOut(taskId: string, priorId?: string): unknown {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// PRI-780: v2-only generation — the pipeline requires an Owner-labelled pack
|
|
138
|
+
// and the artificer output must satisfy the v2 contract (case parity incl.
|
|
139
|
+
// ruleContext + verbatim evidenceRefs).
|
|
140
|
+
const PIPE_RULE_CONTEXT = {
|
|
141
|
+
version: 2 as const,
|
|
142
|
+
history: { status: 'available' as const, truncated: false, calls: [] },
|
|
143
|
+
facts: { priorReadOfTarget: 'unknown' as const, readCount: 0, writeCount: 0, uniqueWritePathCount: 0, sameActionBlockCount: null },
|
|
144
|
+
};
|
|
145
|
+
const PIPE_POS_CASE = { caseId: 'pos-1', kind: 'positive' as const, toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' as const, ruleContext: PIPE_RULE_CONTEXT };
|
|
146
|
+
const PIPE_NEG_CASE = { caseId: 'neg-1', kind: 'negative' as const, toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' as const, ruleContext: PIPE_RULE_CONTEXT };
|
|
147
|
+
const PIPE_PACK = {
|
|
148
|
+
sourceNegativeCase: PIPE_NEG_CASE,
|
|
149
|
+
ownerDesiredOutcome: 'block system path writes',
|
|
150
|
+
positiveCounterexamples: [PIPE_POS_CASE],
|
|
151
|
+
evidenceRefs: ['pain://pipe-1'],
|
|
152
|
+
redactionNotes: [],
|
|
153
|
+
};
|
|
154
|
+
|
|
137
155
|
function artificerV2(taskId: string, priorId?: string): unknown {
|
|
138
156
|
return {
|
|
139
157
|
taskId, sourceScribeArtifactId: requireLineage(priorId, 'sourceScribeArtifactId'),
|
|
158
|
+
requiresContextVersion: 2,
|
|
159
|
+
evidenceRefs: ['pain://pipe-1'],
|
|
140
160
|
implementationPlan: { summary: 'Block /etc writes', targetSurface: 'rule-host', changes: ['matcher'], tests: ['unit'], rolloutNotes: ['shadow'], confidence: 0.85 },
|
|
141
161
|
implementationCode: 'function evaluate(input, helpers) { const p = String(input?.action?.paramsSummary?.path ?? input?.action?.normalizedPath ?? ""); if (p.startsWith("/etc")) return { decision: "block", matched: true, reason: "system path" }; const ctx = input?.context; if (ctx && ctx.facts && ctx.facts.priorReadOfTarget === "no") { return { decision: "block", matched: true, reason: "no prior read" }; } return { decision: "allow", matched: false, reason: "ok" }; }',
|
|
142
162
|
implementationSummary: 'Block system path writes',
|
|
143
|
-
goldenTraceCases: [
|
|
144
|
-
{ caseId: 'pos-1', kind: 'positive', toolName: 'write_file', params: { path: '/project/f.txt' }, expectedDecision: 'allow' },
|
|
145
|
-
{ caseId: 'neg-1', kind: 'negative', toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' },
|
|
146
|
-
],
|
|
163
|
+
goldenTraceCases: [PIPE_POS_CASE, PIPE_NEG_CASE],
|
|
147
164
|
affectedTools: ['write_file'],
|
|
148
165
|
sourceTrace: { scribeArtifactId: requireLineage(priorId, 'sourceTrace.scribeArtifactId') },
|
|
149
166
|
risks: [], generatedAt: new Date().toISOString(),
|
|
@@ -266,6 +283,7 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
|
|
|
266
283
|
workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
|
|
267
284
|
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
268
285
|
codeRuleCapability: capability,
|
|
286
|
+
behaviorExamplePack: PIPE_PACK,
|
|
269
287
|
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
270
288
|
});
|
|
271
289
|
|
|
@@ -320,6 +338,7 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
|
|
|
320
338
|
painId: 'pain-feedback-001',
|
|
321
339
|
runtimeAdapter: adapter,
|
|
322
340
|
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
341
|
+
behaviorExamplePack: PIPE_PACK,
|
|
323
342
|
channel: 'code_tool_hook',
|
|
324
343
|
pollIntervalMs: 5,
|
|
325
344
|
timeoutMs: 1000,
|
|
@@ -349,6 +368,7 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
|
|
|
349
368
|
workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
|
|
350
369
|
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
351
370
|
codeRuleCapability: capability,
|
|
371
|
+
behaviorExamplePack: PIPE_PACK,
|
|
352
372
|
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
353
373
|
});
|
|
354
374
|
|
|
@@ -376,6 +396,7 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
|
|
|
376
396
|
workspaceDir: tmpDir, painId: 'pain-test-001', runtimeAdapter: adapter,
|
|
377
397
|
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
378
398
|
codeRuleCapability: capability,
|
|
399
|
+
behaviorExamplePack: PIPE_PACK,
|
|
379
400
|
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
380
401
|
});
|
|
381
402
|
|
|
@@ -595,6 +616,7 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
|
|
|
595
616
|
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
596
617
|
maxStageRetries: 2,
|
|
597
618
|
codeRuleCapability: capability,
|
|
619
|
+
behaviorExamplePack: PIPE_PACK,
|
|
598
620
|
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
599
621
|
});
|
|
600
622
|
|
|
@@ -725,6 +747,7 @@ describe('runRuleHostPipeline — outputLanguage reaches adapter.startRun messag
|
|
|
725
747
|
workspaceDir: tmpDir, painId: 'pain-lang-en', runtimeAdapter: adapter,
|
|
726
748
|
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
727
749
|
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
750
|
+
behaviorExamplePack: PIPE_PACK,
|
|
728
751
|
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
729
752
|
});
|
|
730
753
|
expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
|
|
@@ -771,6 +794,7 @@ describe('runRuleHostPipeline — outputLanguage reaches adapter.startRun messag
|
|
|
771
794
|
workspaceDir: tmpDir, painId: 'pain-lang-zh', runtimeAdapter: adapter,
|
|
772
795
|
channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
|
|
773
796
|
codeRuleCapability: { enabled: true, artificerAdapter: adapter },
|
|
797
|
+
behaviorExamplePack: PIPE_PACK,
|
|
774
798
|
onStoreReady: (store) => { adapter.artifactStore = store; },
|
|
775
799
|
});
|
|
776
800
|
expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
|