@principles/host-runtime 0.7.14 → 0.8.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/active-principle-prompt.d.ts +2 -0
- package/dist/active-principle-prompt.js +1 -1
- package/dist/host-liveness-contract.d.ts +27 -0
- package/dist/host-liveness-contract.js +38 -12
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -0
- package/dist/internalization-consumer-cycle.js +6 -0
- package/dist/intervention-evidence-ingress.d.ts +41 -0
- package/dist/intervention-evidence-ingress.js +272 -0
- package/dist/prompt-activation-deliverability.d.ts +106 -0
- package/dist/prompt-activation-deliverability.js +176 -0
- package/dist/prompt-injection-event-evidence.d.ts +51 -0
- package/dist/prompt-injection-event-evidence.js +141 -0
- package/dist/prompt-injection-projection.d.ts +135 -19
- package/dist/prompt-injection-projection.js +224 -20
- package/dist/prompt-injection-status.d.ts +44 -0
- package/dist/prompt-injection-status.js +147 -0
- package/package.json +2 -2
|
@@ -1,7 +1,161 @@
|
|
|
1
|
-
import { RUNTIME_V2_PRINCIPLE_BUDGET, computeFeatureFlagsFromConfig, trimToBudget, } from '@principles/core/runtime-v2';
|
|
1
|
+
import { RUNTIME_V2_PRINCIPLE_BUDGET, computeFeatureFlagsFromConfig, renderPrinciplesToDirectives, trimToBudget, } from '@principles/core/runtime-v2';
|
|
2
2
|
import { escapeXml } from '@principles/core/prompt-builder';
|
|
3
3
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
4
|
-
import { buildActivePrinciplePromptContext, readPromptActivationCandidates } from './active-principle-prompt.js';
|
|
4
|
+
import { buildActivePrinciplePromptContext, readPromptActivationCandidates, MAX_SHARED_DIAGNOSTIC_IDS } from './active-principle-prompt.js';
|
|
5
|
+
import { readWorkspaceHostKindFacts } from './host-liveness-contract.js';
|
|
6
|
+
export const PROMPT_INJECTION_TARGET_HOSTS = ['openclaw', 'codex'];
|
|
7
|
+
export const PROMPT_INJECTION_UNIT = 'utf16_code_units';
|
|
8
|
+
/**
|
|
9
|
+
* THE one route formula (P4): Codex always injects through the shared route
|
|
10
|
+
* (codex-adapter has no flag branch); OpenClaw routes by its
|
|
11
|
+
* `abstraction_layer_v1` gate. Every consumer that needs "which route would
|
|
12
|
+
* host X take" must call this — never re-type the ternary.
|
|
13
|
+
*/
|
|
14
|
+
export function promptRouteForHost(hostKind, flagEnabled) {
|
|
15
|
+
if (hostKind === 'codex')
|
|
16
|
+
return 'shared_render';
|
|
17
|
+
return flagEnabled ? 'shared_render' : 'legacy_trim';
|
|
18
|
+
}
|
|
19
|
+
function standardHostRoutes(flagEnabled) {
|
|
20
|
+
return [
|
|
21
|
+
{ hostKind: 'openclaw', route: promptRouteForHost('openclaw', flagEnabled) },
|
|
22
|
+
{ hostKind: 'codex', route: promptRouteForHost('codex', flagEnabled) },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the effective route for this workspace. Pure read of existing
|
|
27
|
+
* config + provenance facts — no new registry, no persisted state.
|
|
28
|
+
*/
|
|
29
|
+
export function resolvePromptInjectionRouteDecision(input) {
|
|
30
|
+
const config = loadPdConfigForPlugin(input.workspaceDir);
|
|
31
|
+
const { flags } = computeFeatureFlagsFromConfig(config.effective);
|
|
32
|
+
const flagEnabled = flags.abstraction_layer_v1?.enabled === true;
|
|
33
|
+
const facts = readWorkspaceHostKindFacts(input.workspaceDir);
|
|
34
|
+
if (!config.ok) {
|
|
35
|
+
const [error] = config.errors;
|
|
36
|
+
return {
|
|
37
|
+
status: 'unconfirmed',
|
|
38
|
+
flagEnabled,
|
|
39
|
+
declaredHostKinds: facts.ok ? facts.declaredKinds : [],
|
|
40
|
+
effectiveHostKinds: facts.ok ? facts.effectiveKinds : [],
|
|
41
|
+
...(!facts.ok ? { hostFactsStatus: 'unreadable' } : {}),
|
|
42
|
+
unconfirmedReason: `config_unreadable: ${error?.reason ?? 'workspace config is invalid'}`,
|
|
43
|
+
nextAction: error?.nextAction ?? 'repair .pd/config.yaml before measuring prompt injection',
|
|
44
|
+
perHost: standardHostRoutes(flagEnabled),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!flags.prompt?.enabled) {
|
|
48
|
+
return {
|
|
49
|
+
status: 'unconfirmed',
|
|
50
|
+
flagEnabled,
|
|
51
|
+
declaredHostKinds: facts.ok ? facts.declaredKinds : [],
|
|
52
|
+
effectiveHostKinds: facts.ok ? facts.effectiveKinds : [],
|
|
53
|
+
...(!facts.ok ? { hostFactsStatus: 'unreadable' } : {}),
|
|
54
|
+
unconfirmedReason: 'prompt_feature_disabled: the prompt injection surface is off',
|
|
55
|
+
nextAction: 'set features.prompt.enabled=true in .pd/config.yaml',
|
|
56
|
+
perHost: standardHostRoutes(flagEnabled),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Explicit request-level host is sufficient to bind the route even when
|
|
60
|
+
// workspace-wide host provenance is unreadable. Config and prompt state
|
|
61
|
+
// above remain mandatory preflight gates.
|
|
62
|
+
if (input.targetHost !== undefined) {
|
|
63
|
+
return {
|
|
64
|
+
status: 'confirmed',
|
|
65
|
+
hostKind: input.targetHost,
|
|
66
|
+
route: promptRouteForHost(input.targetHost, flagEnabled),
|
|
67
|
+
basis: 'explicit_target',
|
|
68
|
+
flagEnabled,
|
|
69
|
+
declaredHostKinds: facts.declaredKinds,
|
|
70
|
+
effectiveHostKinds: facts.ok ? facts.effectiveKinds : [],
|
|
71
|
+
...(!facts.ok ? { hostFactsStatus: 'unreadable' } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (!facts.ok) {
|
|
75
|
+
return {
|
|
76
|
+
status: 'unconfirmed',
|
|
77
|
+
flagEnabled,
|
|
78
|
+
declaredHostKinds: facts.declaredKinds,
|
|
79
|
+
effectiveHostKinds: [],
|
|
80
|
+
hostFactsStatus: 'unreadable',
|
|
81
|
+
unconfirmedReason: `host_facts_unreadable: ${facts.detail}`,
|
|
82
|
+
nextAction: facts.nextAction,
|
|
83
|
+
perHost: standardHostRoutes(flagEnabled),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const knownKinds = facts.effectiveKinds.filter((kind) => kind === 'openclaw' || kind === 'codex');
|
|
87
|
+
const unknownKinds = facts.effectiveKinds.filter((kind) => kind !== 'openclaw' && kind !== 'codex');
|
|
88
|
+
if (unknownKinds.length > 0) {
|
|
89
|
+
return {
|
|
90
|
+
status: 'unconfirmed',
|
|
91
|
+
flagEnabled,
|
|
92
|
+
declaredHostKinds: facts.declaredKinds,
|
|
93
|
+
effectiveHostKinds: facts.effectiveKinds,
|
|
94
|
+
unconfirmedReason: `unknown_host_kind_declared: ${unknownKinds.join(', ')} has no known prompt-injection route`,
|
|
95
|
+
nextAction: 'pass an explicit target host (openclaw|codex) for this request, or remove the unknown host declaration',
|
|
96
|
+
perHost: standardHostRoutes(flagEnabled),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (knownKinds.length === 0) {
|
|
100
|
+
// No host ever operated this workspace. With the flag ON both standard
|
|
101
|
+
// hosts take the shared route, so the route IS confirmed; with the flag
|
|
102
|
+
// OFF they genuinely differ (OpenClaw list vs Codex shared) and there is
|
|
103
|
+
// no fact to pick one with — report unconfirmed instead of guessing.
|
|
104
|
+
if (flagEnabled) {
|
|
105
|
+
return {
|
|
106
|
+
status: 'confirmed',
|
|
107
|
+
route: 'shared_render',
|
|
108
|
+
basis: 'flag_shared_all_hosts',
|
|
109
|
+
flagEnabled,
|
|
110
|
+
declaredHostKinds: facts.declaredKinds,
|
|
111
|
+
effectiveHostKinds: facts.effectiveKinds,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
status: 'unconfirmed',
|
|
116
|
+
flagEnabled,
|
|
117
|
+
declaredHostKinds: facts.declaredKinds,
|
|
118
|
+
effectiveHostKinds: facts.effectiveKinds,
|
|
119
|
+
unconfirmedReason: 'no_host_facts_and_flag_off: no host declaration or host evidence exists, and the applicable hosts would take different routes',
|
|
120
|
+
nextAction: 'pass an explicit target host (openclaw|codex), or let a host run once in this workspace so it persists its declaration',
|
|
121
|
+
perHost: standardHostRoutes(flagEnabled),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (knownKinds.length === 1) {
|
|
125
|
+
const [maybeHostKind] = knownKinds;
|
|
126
|
+
if (maybeHostKind !== undefined) {
|
|
127
|
+
return {
|
|
128
|
+
status: 'confirmed',
|
|
129
|
+
hostKind: maybeHostKind,
|
|
130
|
+
route: promptRouteForHost(maybeHostKind, flagEnabled),
|
|
131
|
+
basis: 'workspace_host_facts',
|
|
132
|
+
flagEnabled,
|
|
133
|
+
declaredHostKinds: facts.declaredKinds,
|
|
134
|
+
effectiveHostKinds: facts.effectiveKinds,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Both openclaw and codex are applicable.
|
|
139
|
+
if (flagEnabled) {
|
|
140
|
+
return {
|
|
141
|
+
status: 'confirmed',
|
|
142
|
+
route: 'shared_render',
|
|
143
|
+
basis: 'flag_shared_all_hosts',
|
|
144
|
+
flagEnabled,
|
|
145
|
+
declaredHostKinds: facts.declaredKinds,
|
|
146
|
+
effectiveHostKinds: facts.effectiveKinds,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
status: 'unconfirmed',
|
|
151
|
+
flagEnabled,
|
|
152
|
+
declaredHostKinds: facts.declaredKinds,
|
|
153
|
+
effectiveHostKinds: facts.effectiveKinds,
|
|
154
|
+
unconfirmedReason: 'multi_host_routes_differ: openclaw would take the legacy list route and codex the shared route under the current flag-off config',
|
|
155
|
+
nextAction: 'pass an explicit target host (openclaw|codex) for this request, or enable abstraction_layer_v1 so both hosts share one route',
|
|
156
|
+
perHost: standardHostRoutes(flagEnabled),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
5
159
|
/**
|
|
6
160
|
* PR-1894: which activations a fair-rotation selector injects across the
|
|
7
161
|
* whole ring. The plugin's round key advances by one per recorded user turn
|
|
@@ -24,18 +178,27 @@ function collectReachableActivationIds(principles, budget) {
|
|
|
24
178
|
}
|
|
25
179
|
return [...reachable];
|
|
26
180
|
}
|
|
181
|
+
function isDiagnosticCapHit(ids) {
|
|
182
|
+
return ids.length >= MAX_SHARED_DIAGNOSTIC_IDS;
|
|
183
|
+
}
|
|
27
184
|
/**
|
|
28
185
|
* PR #1844 follow-up: forecast the prompt-injection budget from the SAME
|
|
29
|
-
* projection the agent injection itself uses
|
|
30
|
-
* `abstraction_layer_v1` flag the OpenClaw plugin's runtime gate reads:
|
|
186
|
+
* projection the agent injection itself uses:
|
|
31
187
|
*
|
|
32
|
-
* - legacy route
|
|
33
|
-
*
|
|
34
|
-
* the
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
188
|
+
* - legacy route: activation candidates from `readPromptActivationCandidates`
|
|
189
|
+
* (same FIFO query + artifact resolve as the plugin's list path) trimmed by
|
|
190
|
+
* the REAL `trimToBudget` — identical input × ordering × trim logic as
|
|
191
|
+
* `openclaw-plugin/src/hooks/prompt.ts`;
|
|
192
|
+
* - shared route: delegate to `buildActivePrinciplePromptContext`, which IS
|
|
193
|
+
* the production shared path's projection source (Codex always; OpenClaw
|
|
194
|
+
* when its `abstraction_layer_v1` gate is on).
|
|
195
|
+
*
|
|
196
|
+
* Route selection here is EXPLICIT: `targetHost: 'codex'` forces the shared
|
|
197
|
+
* route (production truth for Codex), `targetHost: 'openclaw'` follows the
|
|
198
|
+
* workspace flag, and no target follows the flag (the historical behavior —
|
|
199
|
+
* callers that lack host context should prefer
|
|
200
|
+
* `resolveLivePromptInjectionProjection`, which binds the route to real host
|
|
201
|
+
* facts and refuses to guess).
|
|
39
202
|
*
|
|
40
203
|
* Known conservative gap: the plugin's legacy route additionally dedups
|
|
41
204
|
* candidates against the legacy evolution ledger before trimming; the
|
|
@@ -43,19 +206,21 @@ function collectReachableActivationIds(principles, budget) {
|
|
|
43
206
|
* forecast slightly MORE consumption than the real injection (never less).
|
|
44
207
|
*
|
|
45
208
|
* PR-1894 — round-key alignment. The plugin derives its fair-rotation round
|
|
46
|
-
* key from the CURRENT session's turn ordinal
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* input and exposes `selectionPolicy` on the result.
|
|
209
|
+
* key from the CURRENT session's turn ordinal (`nextSessionTurnOrdinal`),
|
|
210
|
+
* which the console has no access to. Passing no key therefore does NOT make
|
|
211
|
+
* the forecast "more conservative": it selects a DIFFERENT policy
|
|
212
|
+
* (`legacy_fifo_prefix_v1`) than the one production runs. The console must
|
|
213
|
+
* either supply the round key or report the policy honestly, so this
|
|
214
|
+
* function takes the key as an explicit input and exposes
|
|
215
|
+
* `selectionPolicy` on the result.
|
|
54
216
|
*/
|
|
55
217
|
export async function buildLivePromptInjectionProjection(input) {
|
|
56
|
-
const
|
|
218
|
+
const flagEnabled = computeFeatureFlagsFromConfig(loadPdConfigForPlugin(input.workspaceDir).effective).flags.abstraction_layer_v1?.enabled === true;
|
|
219
|
+
const sharedRoute = input.targetHost === 'codex' ? true : flagEnabled;
|
|
57
220
|
if (sharedRoute) {
|
|
58
221
|
const context = await buildActivePrinciplePromptContext(input);
|
|
222
|
+
const oversized = context.oversizedActivationIds ?? [];
|
|
223
|
+
const dropped = context.droppedActivationIds ?? [];
|
|
59
224
|
return {
|
|
60
225
|
route: 'shared_render',
|
|
61
226
|
budget: context.budget,
|
|
@@ -71,6 +236,12 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
71
236
|
eventuallyInjectedActivationIds: [],
|
|
72
237
|
eligibleCount: context.eligibleCount ?? 0,
|
|
73
238
|
productionRotates: false,
|
|
239
|
+
unit: PROMPT_INJECTION_UNIT,
|
|
240
|
+
budgetScope: 'full_directive_context',
|
|
241
|
+
oversizedActivationIds: oversized,
|
|
242
|
+
droppedActivationIds: dropped,
|
|
243
|
+
oversizedDiagnosticTruncated: isDiagnosticCapHit(oversized),
|
|
244
|
+
droppedDiagnosticTruncated: isDiagnosticCapHit(dropped),
|
|
74
245
|
};
|
|
75
246
|
}
|
|
76
247
|
const candidates = await readPromptActivationCandidates(input);
|
|
@@ -88,6 +259,12 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
88
259
|
eventuallyInjectedActivationIds: [],
|
|
89
260
|
eligibleCount: 0,
|
|
90
261
|
productionRotates: false,
|
|
262
|
+
unit: PROMPT_INJECTION_UNIT,
|
|
263
|
+
budgetScope: 'selected_lines',
|
|
264
|
+
oversizedActivationIds: [],
|
|
265
|
+
droppedActivationIds: [],
|
|
266
|
+
oversizedDiagnosticTruncated: false,
|
|
267
|
+
droppedDiagnosticTruncated: false,
|
|
91
268
|
};
|
|
92
269
|
}
|
|
93
270
|
// PR-1894: pass the caller's round key so the forecast runs the SAME
|
|
@@ -101,6 +278,7 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
101
278
|
// "queued behind rotation" apart from "structurally starved", otherwise it
|
|
102
279
|
// keeps telling the Owner to deactivate healthy principles.
|
|
103
280
|
const eventuallyInjectedActivationIds = collectReachableActivationIds(principles, RUNTIME_V2_PRINCIPLE_BUDGET);
|
|
281
|
+
const fullRenderChars = renderPrinciplesToDirectives(injected, trimmed.injectedIds, { escapeFn: escapeXml, selfReportInstruction: candidates.selfReportEnabled }).length;
|
|
104
282
|
return {
|
|
105
283
|
route: 'legacy_trim',
|
|
106
284
|
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
@@ -113,5 +291,31 @@ export async function buildLivePromptInjectionProjection(input) {
|
|
|
113
291
|
eventuallyInjectedActivationIds,
|
|
114
292
|
eligibleCount: trimmed.eligibleCount,
|
|
115
293
|
productionRotates: true,
|
|
294
|
+
unit: PROMPT_INJECTION_UNIT,
|
|
295
|
+
budgetScope: 'selected_lines',
|
|
296
|
+
oversizedActivationIds: trimmed.oversizedActivationIds,
|
|
297
|
+
droppedActivationIds: trimmed.droppedActivationIds,
|
|
298
|
+
oversizedDiagnosticTruncated: isDiagnosticCapHit(trimmed.oversizedActivationIds),
|
|
299
|
+
droppedDiagnosticTruncated: isDiagnosticCapHit(trimmed.droppedActivationIds),
|
|
300
|
+
...(injected.length > 0 ? { fullRenderChars } : {}),
|
|
116
301
|
};
|
|
117
302
|
}
|
|
303
|
+
export async function resolveLivePromptInjectionProjection(input) {
|
|
304
|
+
const decision = resolvePromptInjectionRouteDecision(input);
|
|
305
|
+
if (decision.status === 'confirmed') {
|
|
306
|
+
const projection = await buildLivePromptInjectionProjection({
|
|
307
|
+
...input,
|
|
308
|
+
targetHost: decision.hostKind,
|
|
309
|
+
});
|
|
310
|
+
return { status: 'confirmed', decision, projection };
|
|
311
|
+
}
|
|
312
|
+
const hosts = decision.perHost?.map((entry) => entry.hostKind) ?? [...PROMPT_INJECTION_TARGET_HOSTS];
|
|
313
|
+
const perHost = [];
|
|
314
|
+
for (const hostKind of hosts) {
|
|
315
|
+
perHost.push({
|
|
316
|
+
hostKind,
|
|
317
|
+
projection: await buildLivePromptInjectionProjection({ ...input, targetHost: hostKind }),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return { status: 'unconfirmed', decision, perHost };
|
|
321
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, type PromptInjectionRouteName, type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
2
|
+
/**
|
|
3
|
+
* SPEC PD_PROMPT_CAPACITY_V1 R-A3: per-live-activation injection status for
|
|
4
|
+
* the activations surface. Every row carries POSITIVE single-item evidence
|
|
5
|
+
* computed with the production serializer for the bound route — "oversized"
|
|
6
|
+
* means this row's own serialized entry exceeds the budget, "window_excluded"
|
|
7
|
+
* means it fits but the current window is full (AC-06: a row is never deemed
|
|
8
|
+
* healthy merely because it is absent from a bounded diagnostic list), and
|
|
9
|
+
* `route_unconfirmed` surfaces AC-01's honest unknown instead of a guess.
|
|
10
|
+
* No compliance/遵守 counts are produced here — absence of injection logs is
|
|
11
|
+
* NOT reported as "zero compliance" (AC-12 boundary).
|
|
12
|
+
*/
|
|
13
|
+
export type PromptActivationInjectionStatusKind = 'injectable' | 'oversized' | 'window_excluded' | 'resolution_failed' | 'route_unconfirmed';
|
|
14
|
+
export interface PromptActivationInjectionStatus {
|
|
15
|
+
activationId: string;
|
|
16
|
+
principleId?: string;
|
|
17
|
+
status: PromptActivationInjectionStatusKind;
|
|
18
|
+
/** Present for injectable/window_excluded/oversized under a confirmed route. */
|
|
19
|
+
inCurrentWindow?: boolean;
|
|
20
|
+
/** Present when the single-item cost was computable. */
|
|
21
|
+
costChars?: number;
|
|
22
|
+
budget?: number;
|
|
23
|
+
budgetScope?: PromptInjectionBudgetScope;
|
|
24
|
+
route?: PromptInjectionRouteName;
|
|
25
|
+
/** Present for resolution_failed / route_unconfirmed (structured reason). */
|
|
26
|
+
reason?: string;
|
|
27
|
+
/** Present for route_unconfirmed: per-host single-item fit facts. */
|
|
28
|
+
perHostFits?: readonly {
|
|
29
|
+
hostKind: PromptInjectionTargetHost;
|
|
30
|
+
fits: boolean;
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
export interface PromptActivationInjectionStatusReport {
|
|
34
|
+
route: PromptInjectionRouteDecision;
|
|
35
|
+
/** Empty when injection is globally unavailable (prompt flag off / state.db missing) — see `globalReason`. */
|
|
36
|
+
statuses: PromptActivationInjectionStatus[];
|
|
37
|
+
warnings: string[];
|
|
38
|
+
/** Present when no status could be computed at all (fail-loud degradation, rc-9). */
|
|
39
|
+
globalReason?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function readPromptActivationInjectionStatuses(input: {
|
|
42
|
+
workspaceDir: string;
|
|
43
|
+
targetHost?: PromptInjectionTargetHost;
|
|
44
|
+
}): Promise<PromptActivationInjectionStatusReport>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RUNTIME_V2_PRINCIPLE_BUDGET, SqliteActivationStateStore, SqliteConnection, SqlitePIArtifactStore, computeFeatureFlagsFromConfig, filterPromptActivations, resolvePrincipleFromArtifact, } from '@principles/core/runtime-v2';
|
|
4
|
+
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
5
|
+
import { computePromptSingleItemFit } from './prompt-activation-deliverability.js';
|
|
6
|
+
import { PROMPT_INJECTION_TARGET_HOSTS, buildLivePromptInjectionProjection, promptRouteForHost, resolvePromptInjectionRouteDecision, } from './prompt-injection-projection.js';
|
|
7
|
+
export async function readPromptActivationInjectionStatuses(input) {
|
|
8
|
+
const config = loadPdConfigForPlugin(input.workspaceDir);
|
|
9
|
+
const warnings = [];
|
|
10
|
+
if (!config.ok) {
|
|
11
|
+
warnings.push(...config.errors.map((error) => `config_invalid: ${error.reason}; nextAction=${error.nextAction}`));
|
|
12
|
+
}
|
|
13
|
+
const { flags } = computeFeatureFlagsFromConfig(config.effective);
|
|
14
|
+
const selfReportEnabled = flags.principle_receipt_self_report?.enabled === true;
|
|
15
|
+
const route = resolvePromptInjectionRouteDecision(input);
|
|
16
|
+
if (!flags.prompt?.enabled) {
|
|
17
|
+
return {
|
|
18
|
+
route,
|
|
19
|
+
statuses: [],
|
|
20
|
+
warnings,
|
|
21
|
+
globalReason: 'prompt_feature_disabled: the prompt injection surface is off, so no activation can be injected; nextAction=set features.prompt.enabled=true in .pd/config.yaml',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const stateDbPath = path.join(input.workspaceDir, '.pd', 'state.db');
|
|
25
|
+
if (!fs.existsSync(stateDbPath)) {
|
|
26
|
+
return {
|
|
27
|
+
route,
|
|
28
|
+
statuses: [],
|
|
29
|
+
warnings,
|
|
30
|
+
globalReason: 'activation_db_not_found: .pd/state.db is missing, live activations cannot be read; nextAction=initialize_workspace_runtime_state',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const rows = [];
|
|
34
|
+
let connection;
|
|
35
|
+
try {
|
|
36
|
+
connection = new SqliteConnection({ workspaceDir: input.workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
37
|
+
const activations = filterPromptActivations(await new SqliteActivationStateStore(connection).listPromptActivations());
|
|
38
|
+
const artifactStore = new SqlitePIArtifactStore(connection);
|
|
39
|
+
for (const activation of activations) {
|
|
40
|
+
let artifact;
|
|
41
|
+
try {
|
|
42
|
+
artifact = await artifactStore.getArtifactById(activation.artifactId);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
rows.push({ activationId: activation.activationId, failure: `artifact_query_failed: ${message}` });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (artifact === null) {
|
|
50
|
+
rows.push({ activationId: activation.activationId, failure: `artifact_not_found: artifactId=${activation.artifactId}` });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const resolved = resolvePrincipleFromArtifact({
|
|
54
|
+
artifact_id: artifact.artifactId,
|
|
55
|
+
artifact_kind: artifact.artifactKind,
|
|
56
|
+
content_json: artifact.contentJson,
|
|
57
|
+
validation_status: artifact.validationStatus,
|
|
58
|
+
}, activation);
|
|
59
|
+
if (!resolved.ok) {
|
|
60
|
+
rows.push({ activationId: activation.activationId, failure: resolved.warning });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
rows.push({
|
|
64
|
+
activationId: activation.activationId,
|
|
65
|
+
principleId: resolved.principle.principleId,
|
|
66
|
+
text: resolved.principle.text,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
return {
|
|
73
|
+
route,
|
|
74
|
+
statuses: [],
|
|
75
|
+
warnings,
|
|
76
|
+
globalReason: `activation_db_unreadable: ${message}; nextAction=check_workspace_pd_state_db`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
connection?.close();
|
|
81
|
+
}
|
|
82
|
+
if (route.status === 'unconfirmed') {
|
|
83
|
+
const statuses = rows.map((row) => {
|
|
84
|
+
if ('failure' in row) {
|
|
85
|
+
return { activationId: row.activationId, status: 'resolution_failed', reason: row.failure };
|
|
86
|
+
}
|
|
87
|
+
const perHostFits = PROMPT_INJECTION_TARGET_HOSTS.map((hostKind) => {
|
|
88
|
+
const hostRoute = promptRouteForHost(hostKind, route.flagEnabled);
|
|
89
|
+
const { fits } = computePromptSingleItemFit({
|
|
90
|
+
principle: { principleId: row.principleId, text: row.text, artifactId: '', activationId: row.activationId },
|
|
91
|
+
route: hostRoute,
|
|
92
|
+
selfReportEnabled,
|
|
93
|
+
});
|
|
94
|
+
return { hostKind, fits };
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
activationId: row.activationId,
|
|
98
|
+
principleId: row.principleId,
|
|
99
|
+
status: 'route_unconfirmed',
|
|
100
|
+
reason: route.unconfirmedReason ?? 'effective injection route cannot be confirmed',
|
|
101
|
+
perHostFits,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return { route, statuses, warnings };
|
|
105
|
+
}
|
|
106
|
+
const projection = await buildLivePromptInjectionProjection({
|
|
107
|
+
workspaceDir: input.workspaceDir,
|
|
108
|
+
targetHost: route.hostKind,
|
|
109
|
+
});
|
|
110
|
+
warnings.push(...projection.warnings);
|
|
111
|
+
const statuses = rows.map((row) => {
|
|
112
|
+
if ('failure' in row) {
|
|
113
|
+
return { activationId: row.activationId, status: 'resolution_failed', reason: row.failure };
|
|
114
|
+
}
|
|
115
|
+
const { fits, costChars } = computePromptSingleItemFit({
|
|
116
|
+
principle: { principleId: row.principleId, text: row.text, artifactId: '', activationId: row.activationId },
|
|
117
|
+
route: projection.route,
|
|
118
|
+
selfReportEnabled,
|
|
119
|
+
});
|
|
120
|
+
const inCurrentWindow = projection.injectedActivationIds.includes(row.activationId);
|
|
121
|
+
const budgetScope = projection.budgetScope;
|
|
122
|
+
if (!fits) {
|
|
123
|
+
return {
|
|
124
|
+
activationId: row.activationId,
|
|
125
|
+
principleId: row.principleId,
|
|
126
|
+
status: 'oversized',
|
|
127
|
+
inCurrentWindow: false,
|
|
128
|
+
costChars,
|
|
129
|
+
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
130
|
+
budgetScope,
|
|
131
|
+
route: projection.route,
|
|
132
|
+
reason: `single_item_exceeds_budget: this principle's own entry is ${costChars} chars > ${RUNTIME_V2_PRINCIPLE_BUDGET} under the ${projection.route === 'shared_render' ? 'full directive render' : 'list entry'} serialization`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
activationId: row.activationId,
|
|
137
|
+
principleId: row.principleId,
|
|
138
|
+
status: inCurrentWindow ? 'injectable' : 'window_excluded',
|
|
139
|
+
inCurrentWindow,
|
|
140
|
+
costChars,
|
|
141
|
+
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
142
|
+
budgetScope,
|
|
143
|
+
route: projection.route,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
return { route, statuses, warnings };
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"lint": "eslint \"src/**/*.ts\""
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@principles/core": "^1.
|
|
24
|
+
"@principles/core": "^1.291.0",
|
|
25
25
|
"@principles/install-layout": "^0.2.7",
|
|
26
26
|
"better-sqlite3": "^13.0.3",
|
|
27
27
|
"js-yaml": "^5.4.1"
|