@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RUNTIME_V2_PRINCIPLE_BUDGET, SqliteConnection, SqlitePIArtifactStore, computeFeatureFlagsFromConfig, listEntryLine, renderPrinciplesToDirectives, resolvePrincipleFromArtifact, trimToBudget, } from '@principles/core/runtime-v2';
|
|
4
|
+
import { escapeXml } from '@principles/core/prompt-builder';
|
|
5
|
+
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
6
|
+
import { PROMPT_INJECTION_UNIT, promptRouteForHost, resolvePromptInjectionRouteDecision, } from './prompt-injection-projection.js';
|
|
7
|
+
function syntheticPendingActivation(artifactId) {
|
|
8
|
+
return {
|
|
9
|
+
activationId: `pending:${artifactId}`,
|
|
10
|
+
idempotencyKey: `pending:${artifactId}::prompt`,
|
|
11
|
+
artifactId,
|
|
12
|
+
channel: 'prompt',
|
|
13
|
+
action: 'prompt_activate',
|
|
14
|
+
targetRef: `pending://${artifactId}`,
|
|
15
|
+
activatedAt: new Date(0).toISOString(),
|
|
16
|
+
deactivatedAt: null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Resolve the REAL principle identity + text from the artifact content. */
|
|
20
|
+
export function resolvePromptCandidatePrinciple(artifact) {
|
|
21
|
+
return resolvePrincipleFromArtifact({
|
|
22
|
+
artifact_id: artifact.artifactId,
|
|
23
|
+
artifact_kind: artifact.artifactKind,
|
|
24
|
+
content_json: artifact.contentJson,
|
|
25
|
+
validation_status: artifact.validationStatus,
|
|
26
|
+
}, syntheticPendingActivation(artifact.artifactId));
|
|
27
|
+
}
|
|
28
|
+
function selfReportEnabledFor(workspaceDir) {
|
|
29
|
+
return computeFeatureFlagsFromConfig(loadPdConfigForPlugin(workspaceDir).effective).flags.principle_receipt_self_report?.enabled === true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Positive single-item evidence under a given route, using the production
|
|
33
|
+
* selector itself (AC-06: a row is "fits" because its own serialized entry
|
|
34
|
+
* fits — never because it is absent from a bounded diagnostic list).
|
|
35
|
+
*
|
|
36
|
+
* `costChars` is what the single-entry selection would consume, measured the
|
|
37
|
+
* same way the route's projection reports `usedChars` (the joined selected
|
|
38
|
+
* lines). For an entry that does NOT fit, trimToBudget returns only the
|
|
39
|
+
* header line, so the would-be cost is reconstructed from the header line it
|
|
40
|
+
* DID return plus the real entry line (same format authority, listEntryLine).
|
|
41
|
+
*/
|
|
42
|
+
export function computePromptSingleItemFit(input) {
|
|
43
|
+
if (input.route === 'shared_render') {
|
|
44
|
+
const render = renderPrinciplesToDirectives([input.principle], new Set([input.principle.principleId]), { escapeFn: escapeXml, selfReportInstruction: input.selfReportEnabled });
|
|
45
|
+
return { fits: render.length <= RUNTIME_V2_PRINCIPLE_BUDGET, costChars: render.length };
|
|
46
|
+
}
|
|
47
|
+
const trimmed = trimToBudget([input.principle], RUNTIME_V2_PRINCIPLE_BUDGET, escapeXml);
|
|
48
|
+
const fits = trimmed.injectedIds.has(input.principle.principleId);
|
|
49
|
+
const costChars = trimmed.lines.length > 1
|
|
50
|
+
? trimmed.lines.join('\n').length
|
|
51
|
+
: (trimmed.lines[0]?.length ?? 0) + 1 + listEntryLine(input.principle, escapeXml).length;
|
|
52
|
+
return { fits, costChars };
|
|
53
|
+
}
|
|
54
|
+
const UNDELIVERABLE_NEXT_ACTION = 'shorten the principle statement and resubmit, or use the activations page "modify to injectable version" entry to create a replacement artifact (the approval facts are preserved)';
|
|
55
|
+
/**
|
|
56
|
+
* Precheck one artifact against the workspace's effective injection route.
|
|
57
|
+
* Pure read — no state mutation, safe to call before any governance write.
|
|
58
|
+
*/
|
|
59
|
+
export async function checkPromptArtifactDeliverability(input) {
|
|
60
|
+
if (!input.artifact) {
|
|
61
|
+
return {
|
|
62
|
+
status: 'unconfirmed',
|
|
63
|
+
reason: 'artifact_unavailable: artifact could not be read from the store',
|
|
64
|
+
nextAction: 'check pi_artifacts for the approval artifact, then retry',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const resolved = resolvePromptCandidatePrinciple(input.artifact);
|
|
68
|
+
if (!resolved.ok) {
|
|
69
|
+
return {
|
|
70
|
+
status: 'unconfirmed',
|
|
71
|
+
reason: `artifact_content_unresolved: ${resolved.warning}`,
|
|
72
|
+
nextAction: 'fix the artifact content (kind/validation/principleId/statement), then retry',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const selfReportEnabled = selfReportEnabledFor(input.workspaceDir);
|
|
76
|
+
const decision = resolvePromptInjectionRouteDecision({
|
|
77
|
+
workspaceDir: input.workspaceDir,
|
|
78
|
+
targetHost: input.targetHost,
|
|
79
|
+
});
|
|
80
|
+
if (decision.status === 'confirmed' && decision.route !== undefined) {
|
|
81
|
+
const { fits, costChars } = computePromptSingleItemFit({
|
|
82
|
+
principle: resolved.principle,
|
|
83
|
+
route: decision.route,
|
|
84
|
+
selfReportEnabled,
|
|
85
|
+
});
|
|
86
|
+
const budgetScope = decision.route === 'shared_render'
|
|
87
|
+
? 'full_directive_context'
|
|
88
|
+
: 'selected_lines';
|
|
89
|
+
if (fits) {
|
|
90
|
+
return {
|
|
91
|
+
status: 'deliverable',
|
|
92
|
+
route: decision.route,
|
|
93
|
+
...(decision.hostKind !== undefined ? { hostKind: decision.hostKind } : {}),
|
|
94
|
+
...(decision.basis !== undefined ? { basis: decision.basis } : {}),
|
|
95
|
+
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
96
|
+
costChars,
|
|
97
|
+
budgetScope,
|
|
98
|
+
unit: PROMPT_INJECTION_UNIT,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
status: 'undeliverable',
|
|
103
|
+
reason: 'single_item_exceeds_budget',
|
|
104
|
+
route: decision.route,
|
|
105
|
+
...(decision.hostKind !== undefined ? { hostKind: decision.hostKind } : {}),
|
|
106
|
+
...(decision.basis !== undefined ? { basis: decision.basis } : {}),
|
|
107
|
+
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
108
|
+
costChars,
|
|
109
|
+
overByChars: costChars - RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
110
|
+
budgetScope,
|
|
111
|
+
unit: PROMPT_INJECTION_UNIT,
|
|
112
|
+
nextAction: UNDELIVERABLE_NEXT_ACTION,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Route unconfirmed: report per-host fit for every standard host instead of
|
|
116
|
+
// guessing (R-A1) — the write gate refuses, the UI can show both facts.
|
|
117
|
+
const hosts = ['openclaw', 'codex'];
|
|
118
|
+
const perHost = hosts.map((hostKind) => {
|
|
119
|
+
const route = promptRouteForHost(hostKind, decision.flagEnabled);
|
|
120
|
+
const { fits, costChars } = computePromptSingleItemFit({
|
|
121
|
+
principle: resolved.principle,
|
|
122
|
+
route,
|
|
123
|
+
selfReportEnabled,
|
|
124
|
+
});
|
|
125
|
+
return { hostKind, fits, costChars };
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
status: 'unconfirmed',
|
|
129
|
+
reason: `route_unconfirmed: ${decision.unconfirmedReason ?? 'effective injection route cannot be confirmed'}`,
|
|
130
|
+
nextAction: decision.nextAction ?? 'pass an explicit target host (openclaw|codex)',
|
|
131
|
+
perHost,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Load one artifact read-only from `<workspace>/.pd/state.db`. Failures carry a reason (rc-9). */
|
|
135
|
+
export async function loadPromptArtifactById(workspaceDir, artifactId) {
|
|
136
|
+
const stateDbPath = path.join(workspaceDir, '.pd', 'state.db');
|
|
137
|
+
if (!fs.existsSync(stateDbPath)) {
|
|
138
|
+
return { ok: false, reason: `activation_db_not_found: ${stateDbPath}; nextAction=initialize_workspace_runtime_state` };
|
|
139
|
+
}
|
|
140
|
+
let connection;
|
|
141
|
+
try {
|
|
142
|
+
connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
143
|
+
const record = await new SqlitePIArtifactStore(connection).getArtifactById(artifactId);
|
|
144
|
+
if (record) {
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
artifact: {
|
|
148
|
+
artifactId: record.artifactId,
|
|
149
|
+
artifactKind: record.artifactKind,
|
|
150
|
+
contentJson: record.contentJson,
|
|
151
|
+
validationStatus: record.validationStatus,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { ok: false, reason: `artifact_not_found: artifactId=${artifactId}; nextAction=check_pi_artifacts_table` };
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
+
return { ok: false, reason: `artifact_read_failed: ${message}; nextAction=check_workspace_pd_state_db` };
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
connection?.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Combined load + precheck for write gates that only hold an artifact id (CLI approve). */
|
|
166
|
+
export async function checkPromptArtifactDeliverabilityById(input) {
|
|
167
|
+
const loaded = await loadPromptArtifactById(input.workspaceDir, input.artifactId);
|
|
168
|
+
if (!loaded.ok) {
|
|
169
|
+
return {
|
|
170
|
+
status: 'unconfirmed',
|
|
171
|
+
reason: loaded.reason,
|
|
172
|
+
nextAction: 'fix the artifact store readability, then retry the approval',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return checkPromptArtifactDeliverability({ ...input, artifact: loaded.artifact });
|
|
176
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PD_PROMPT_CAPACITY_V1 Phase C / AC-12: read-model over the EXISTING
|
|
3
|
+
* injection-event JSONL (`<stateDir>/logs/events_<date>.jsonl`, written by the
|
|
4
|
+
* OpenClaw EventLog and the Codex shared path via appendEventLogLine).
|
|
5
|
+
*
|
|
6
|
+
* Honesty rules this model enforces (SPEC §6):
|
|
7
|
+
* - NO logs ≠ zero injections: an activation with no events is simply absent
|
|
8
|
+
* from `rows`; the caller reports "unknown", never "0 proven injections".
|
|
9
|
+
* - Duplicate lines (identical serialized payloads — rollover/replay) count
|
|
10
|
+
* once; the dedupe count is reported, not hidden.
|
|
11
|
+
* - Statistics never mix entities: rows are keyed by (workspaceDir,
|
|
12
|
+
* activationId); the same runId across different workspaces/sessions stays
|
|
13
|
+
* separate.
|
|
14
|
+
* - Old events missing identity fields (runId) still count as injection FACTS
|
|
15
|
+
* but flag `runIdComplete` false — the numbers degrade to "known counts,
|
|
16
|
+
* incomplete binding", never to zero.
|
|
17
|
+
* - Model self-report (principle_applications kind self_reported) is a
|
|
18
|
+
* separate signal and is deliberately NOT read here: self-report ≠
|
|
19
|
+
* compliance.
|
|
20
|
+
*
|
|
21
|
+
* Untrusted input discipline: every line is parsed as `unknown` and narrowed
|
|
22
|
+
* with typeof guards (rc-1..rc-5); structurally invalid lines are counted in
|
|
23
|
+
* `parseFailures` and skipped loudly, never guessed (rc-3).
|
|
24
|
+
*/
|
|
25
|
+
export interface InjectionEvidenceRow {
|
|
26
|
+
workspaceDir: string;
|
|
27
|
+
activationId: string;
|
|
28
|
+
hostKind: 'openclaw' | 'codex' | 'unknown';
|
|
29
|
+
hostIdentityComplete: boolean;
|
|
30
|
+
eventIdentityComplete: boolean;
|
|
31
|
+
/** Deduplicated injection facts: this activation appeared in the injected set. */
|
|
32
|
+
provenInjections: number;
|
|
33
|
+
/** Raw matching events before dedupe. */
|
|
34
|
+
rawEventCount: number;
|
|
35
|
+
duplicateEventsDeduped: number;
|
|
36
|
+
sessions: string[];
|
|
37
|
+
runIds: string[];
|
|
38
|
+
/** False when any counted event lacked runId (old events / hosts without turn ids). */
|
|
39
|
+
runIdComplete: boolean;
|
|
40
|
+
lastEventAt: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface InjectionEventEvidenceReport {
|
|
43
|
+
filesRead: string[];
|
|
44
|
+
lines: number;
|
|
45
|
+
parseFailures: number;
|
|
46
|
+
rows: InjectionEvidenceRow[];
|
|
47
|
+
note: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function readInjectionEventEvidence(input: {
|
|
50
|
+
stateDir: string;
|
|
51
|
+
}): InjectionEventEvidenceReport;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function nonEmptyString(value) {
|
|
7
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
export function readInjectionEventEvidence(input) {
|
|
10
|
+
const logsRoot = path.resolve(input.stateDir, 'logs');
|
|
11
|
+
const filesRead = [];
|
|
12
|
+
if (!fs.existsSync(logsRoot)) {
|
|
13
|
+
return {
|
|
14
|
+
filesRead,
|
|
15
|
+
lines: 0,
|
|
16
|
+
parseFailures: 0,
|
|
17
|
+
rows: [],
|
|
18
|
+
note: 'no_logs: the injection-event log directory does not exist — injection/coverage numbers are UNKNOWN, not zero',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const files = fs.readdirSync(logsRoot)
|
|
22
|
+
.filter((name) => name.startsWith('events_') && name.endsWith('.jsonl'))
|
|
23
|
+
.sort();
|
|
24
|
+
let lines = 0;
|
|
25
|
+
let parseFailures = 0;
|
|
26
|
+
// Rows are separated by workspace, host, and activation. Old hostless events
|
|
27
|
+
// remain in an explicit unknown bucket and cannot merge with either host.
|
|
28
|
+
const rows = new Map();
|
|
29
|
+
for (const name of files) {
|
|
30
|
+
// Containment guard: only bare event filenames inside the resolved logs
|
|
31
|
+
// root are ever read — a crafted name cannot escape the directory.
|
|
32
|
+
const filePath = path.resolve(logsRoot, name);
|
|
33
|
+
if (!filePath.startsWith(logsRoot + path.sep) || path.basename(name) !== name)
|
|
34
|
+
continue;
|
|
35
|
+
filesRead.push(filePath);
|
|
36
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
37
|
+
for (const line of raw.split('\n')) {
|
|
38
|
+
if (line.trim().length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
lines += 1;
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
parseFailures += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!isRecord(parsed)) {
|
|
50
|
+
parseFailures += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (nonEmptyString(parsed.type) !== 'runtime_v2_prompt_activations_injected')
|
|
54
|
+
continue;
|
|
55
|
+
const ts = nonEmptyString(parsed.ts) ?? null;
|
|
56
|
+
const { data } = parsed;
|
|
57
|
+
if (!isRecord(data)) {
|
|
58
|
+
parseFailures += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const workspaceDir = nonEmptyString(data.workspaceDir);
|
|
62
|
+
const sessionId = nonEmptyString(data.sessionId) ?? nonEmptyString(parsed.sessionId);
|
|
63
|
+
const runId = nonEmptyString(data.runId);
|
|
64
|
+
const eventId = nonEmptyString(data.eventId);
|
|
65
|
+
const rawHostKind = data.hostKind;
|
|
66
|
+
const hostKind = rawHostKind === 'openclaw' || rawHostKind === 'codex' ? rawHostKind : 'unknown';
|
|
67
|
+
const { activationIds } = data;
|
|
68
|
+
if (workspaceDir === undefined || sessionId === undefined || !Array.isArray(activationIds)
|
|
69
|
+
|| !activationIds.every((id) => typeof id === 'string')) {
|
|
70
|
+
// rc-3/rc-4: identity binding incomplete → loud skip, never a guess.
|
|
71
|
+
parseFailures += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const dedupeKey = eventId !== undefined
|
|
75
|
+
? JSON.stringify(['event', hostKind, eventId])
|
|
76
|
+
: JSON.stringify(['legacy', hostKind, workspaceDir, sessionId, runId ?? null, ts, activationIds.join(','), data.injectedCharCount, data.selectionPolicy ?? null, data.rotationStartIndex ?? null]);
|
|
77
|
+
for (const activationId of activationIds) {
|
|
78
|
+
const rowKey = JSON.stringify([workspaceDir, hostKind, activationId]);
|
|
79
|
+
let row = rows.get(rowKey);
|
|
80
|
+
if (row === undefined) {
|
|
81
|
+
row = {
|
|
82
|
+
workspaceDir,
|
|
83
|
+
activationId,
|
|
84
|
+
hostKind,
|
|
85
|
+
hostIdentityComplete: hostKind !== 'unknown',
|
|
86
|
+
eventIdentityComplete: true,
|
|
87
|
+
provenInjections: 0,
|
|
88
|
+
rawEventCount: 0,
|
|
89
|
+
duplicateEventsDeduped: 0,
|
|
90
|
+
sessions: [],
|
|
91
|
+
runIds: [],
|
|
92
|
+
runIdComplete: true,
|
|
93
|
+
lastEventAt: null,
|
|
94
|
+
seen: new Set(),
|
|
95
|
+
};
|
|
96
|
+
rows.set(rowKey, row);
|
|
97
|
+
}
|
|
98
|
+
row.rawEventCount += 1;
|
|
99
|
+
if (hostKind === 'unknown')
|
|
100
|
+
row.hostIdentityComplete = false;
|
|
101
|
+
if (eventId === undefined)
|
|
102
|
+
row.eventIdentityComplete = false;
|
|
103
|
+
if (!row.seen.has(dedupeKey)) {
|
|
104
|
+
row.seen.add(dedupeKey);
|
|
105
|
+
row.provenInjections += 1;
|
|
106
|
+
if (!row.sessions.includes(sessionId))
|
|
107
|
+
row.sessions.push(sessionId);
|
|
108
|
+
if (runId !== undefined && !row.runIds.includes(runId))
|
|
109
|
+
row.runIds.push(runId);
|
|
110
|
+
if (ts !== null && (row.lastEventAt === null || ts > row.lastEventAt))
|
|
111
|
+
row.lastEventAt = ts;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
row.duplicateEventsDeduped += 1;
|
|
115
|
+
}
|
|
116
|
+
if (runId === undefined)
|
|
117
|
+
row.runIdComplete = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
filesRead,
|
|
123
|
+
lines,
|
|
124
|
+
parseFailures,
|
|
125
|
+
rows: [...rows.values()].map((row) => ({
|
|
126
|
+
workspaceDir: row.workspaceDir,
|
|
127
|
+
activationId: row.activationId,
|
|
128
|
+
hostKind: row.hostKind,
|
|
129
|
+
hostIdentityComplete: row.hostIdentityComplete,
|
|
130
|
+
eventIdentityComplete: row.eventIdentityComplete,
|
|
131
|
+
provenInjections: row.provenInjections,
|
|
132
|
+
rawEventCount: row.rawEventCount,
|
|
133
|
+
duplicateEventsDeduped: row.duplicateEventsDeduped,
|
|
134
|
+
sessions: row.sessions,
|
|
135
|
+
runIds: row.runIds,
|
|
136
|
+
runIdComplete: row.runIdComplete,
|
|
137
|
+
lastEventAt: row.lastEventAt,
|
|
138
|
+
})),
|
|
139
|
+
note: 'absence from rows means UNKNOWN (no logs), never zero; self-report is a separate signal and is not compliance evidence; rows never merge workspaces',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,7 +1,69 @@
|
|
|
1
1
|
import { type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
2
|
+
/** Hosts with a production prompt-injection path today (shared GovernanceHostKind boundary). */
|
|
3
|
+
export type PromptInjectionTargetHost = 'openclaw' | 'codex';
|
|
4
|
+
export declare const PROMPT_INJECTION_TARGET_HOSTS: readonly PromptInjectionTargetHost[];
|
|
5
|
+
export type PromptInjectionRouteName = 'legacy_trim' | 'shared_render';
|
|
6
|
+
/**
|
|
7
|
+
* Which serialization the budget is charged against (SPEC R-A2): the legacy
|
|
8
|
+
* list route bills only the selected `- [id] text` lines (the directive
|
|
9
|
+
* wrapper and the self-report footer live OUTSIDE that budget on that route),
|
|
10
|
+
* while the shared route bills the full rendered directive context including
|
|
11
|
+
* wrapper and footer. Both are UTF-16 code units (`String.prototype.length`).
|
|
12
|
+
*/
|
|
13
|
+
export type PromptInjectionBudgetScope = 'selected_lines' | 'full_directive_context';
|
|
14
|
+
export declare const PROMPT_INJECTION_UNIT: 'utf16_code_units';
|
|
15
|
+
/**
|
|
16
|
+
* AC-01: the effective injection route is a fact about WHICH HOST will run
|
|
17
|
+
* the prompt build, not about the workspace flag alone. Codex always injects
|
|
18
|
+
* through the shared route (`codex-adapter/src/pd-hook.ts`); OpenClaw routes
|
|
19
|
+
* by its `abstraction_layer_v1` gate (`openclaw-plugin/src/index.ts`). Host
|
|
20
|
+
* facts come from the existing durable provenance (declarations ∪ pain
|
|
21
|
+
* evidence — `readWorkspaceHostKindFacts`); an explicit request-level target
|
|
22
|
+
* overrides them. When the applicable hosts disagree (flag OFF + both hosts
|
|
23
|
+
* possible, or no host facts at all) the decision is `unconfirmed` and every
|
|
24
|
+
* consumer must say so instead of guessing FIFO or rotation (SPEC R-A1).
|
|
25
|
+
*/
|
|
26
|
+
export interface PromptInjectionRouteDecision {
|
|
27
|
+
status: 'confirmed' | 'unconfirmed';
|
|
28
|
+
/** Present when status === 'confirmed' AND a single host is bound. */
|
|
29
|
+
hostKind?: PromptInjectionTargetHost;
|
|
30
|
+
/** Present when status === 'confirmed'. */
|
|
31
|
+
route?: PromptInjectionRouteName;
|
|
32
|
+
basis?: 'explicit_target' | 'workspace_host_facts' | 'flag_shared_all_hosts';
|
|
33
|
+
/** Present when explicit binding succeeds while workspace-wide host facts are unreadable. */
|
|
34
|
+
hostFactsStatus?: 'unreadable';
|
|
35
|
+
/** Raw `abstraction_layer_v1` flag state (context for the OpenClaw branch). */
|
|
36
|
+
flagEnabled: boolean;
|
|
37
|
+
declaredHostKinds: readonly string[];
|
|
38
|
+
effectiveHostKinds: readonly string[];
|
|
39
|
+
/** Present when status === 'unconfirmed'. */
|
|
40
|
+
unconfirmedReason?: string;
|
|
41
|
+
/** Present when status === 'unconfirmed'. */
|
|
42
|
+
nextAction?: string;
|
|
43
|
+
/** Route each standard host WOULD take (context for unconfirmed decisions). */
|
|
44
|
+
perHost?: readonly {
|
|
45
|
+
hostKind: PromptInjectionTargetHost;
|
|
46
|
+
route: PromptInjectionRouteName;
|
|
47
|
+
}[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* THE one route formula (P4): Codex always injects through the shared route
|
|
51
|
+
* (codex-adapter has no flag branch); OpenClaw routes by its
|
|
52
|
+
* `abstraction_layer_v1` gate. Every consumer that needs "which route would
|
|
53
|
+
* host X take" must call this — never re-type the ternary.
|
|
54
|
+
*/
|
|
55
|
+
export declare function promptRouteForHost(hostKind: PromptInjectionTargetHost, flagEnabled: boolean): PromptInjectionRouteName;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the effective route for this workspace. Pure read of existing
|
|
58
|
+
* config + provenance facts — no new registry, no persisted state.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolvePromptInjectionRouteDecision(input: {
|
|
61
|
+
workspaceDir: string;
|
|
62
|
+
targetHost?: PromptInjectionTargetHost;
|
|
63
|
+
}): PromptInjectionRouteDecision;
|
|
2
64
|
export interface PromptInjectionProjection {
|
|
3
|
-
/** Which real injection route
|
|
4
|
-
route:
|
|
65
|
+
/** Which real injection route THIS projection ran (explicit target or flag). */
|
|
66
|
+
route: PromptInjectionRouteName;
|
|
5
67
|
budget: number;
|
|
6
68
|
/**
|
|
7
69
|
* Char count the REAL prompt path would consume: legacy route counts the
|
|
@@ -46,19 +108,48 @@ export interface PromptInjectionProjection {
|
|
|
46
108
|
* or starved. They differ exactly in the case this whole fix is about.
|
|
47
109
|
*/
|
|
48
110
|
productionRotates: boolean;
|
|
111
|
+
/** R-A2: the unit `usedChars`/`budget` are counted in. */
|
|
112
|
+
unit: typeof PROMPT_INJECTION_UNIT;
|
|
113
|
+
/** R-A2: which serialization the budget is charged against on this route. */
|
|
114
|
+
budgetScope: PromptInjectionBudgetScope;
|
|
115
|
+
/**
|
|
116
|
+
* R-A3: activation ids whose own entry cannot fit the budget even in an
|
|
117
|
+
* empty payload, independent of `truncated` (bounded diagnostic list —
|
|
118
|
+
* absence from a capped list is NOT evidence of fitting; see
|
|
119
|
+
* `oversizedDiagnosticTruncated`).
|
|
120
|
+
*/
|
|
121
|
+
oversizedActivationIds: string[];
|
|
122
|
+
/** R-A3: activation ids dropped for insufficient remaining budget (bounded). */
|
|
123
|
+
droppedActivationIds: string[];
|
|
124
|
+
/** True when a bounded diagnostic list hit its cap — more ids may exist. */
|
|
125
|
+
oversizedDiagnosticTruncated: boolean;
|
|
126
|
+
droppedDiagnosticTruncated: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* R-A2 (list route only): length of the FULL directive render of the
|
|
129
|
+
* injected set under the workspace self-report flag — the whole PD output
|
|
130
|
+
* block, NOT the host request and NOT tokens. Undefined on the shared
|
|
131
|
+
* route (where `usedChars` already IS the full render).
|
|
132
|
+
*/
|
|
133
|
+
fullRenderChars?: number;
|
|
49
134
|
}
|
|
50
135
|
/**
|
|
51
136
|
* PR #1844 follow-up: forecast the prompt-injection budget from the SAME
|
|
52
|
-
* projection the agent injection itself uses
|
|
53
|
-
*
|
|
137
|
+
* projection the agent injection itself uses:
|
|
138
|
+
*
|
|
139
|
+
* - legacy route: activation candidates from `readPromptActivationCandidates`
|
|
140
|
+
* (same FIFO query + artifact resolve as the plugin's list path) trimmed by
|
|
141
|
+
* the REAL `trimToBudget` — identical input × ordering × trim logic as
|
|
142
|
+
* `openclaw-plugin/src/hooks/prompt.ts`;
|
|
143
|
+
* - shared route: delegate to `buildActivePrinciplePromptContext`, which IS
|
|
144
|
+
* the production shared path's projection source (Codex always; OpenClaw
|
|
145
|
+
* when its `abstraction_layer_v1` gate is on).
|
|
54
146
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* which IS the production shared path's projection source.
|
|
147
|
+
* Route selection here is EXPLICIT: `targetHost: 'codex'` forces the shared
|
|
148
|
+
* route (production truth for Codex), `targetHost: 'openclaw'` follows the
|
|
149
|
+
* workspace flag, and no target follows the flag (the historical behavior —
|
|
150
|
+
* callers that lack host context should prefer
|
|
151
|
+
* `resolveLivePromptInjectionProjection`, which binds the route to real host
|
|
152
|
+
* facts and refuses to guess).
|
|
62
153
|
*
|
|
63
154
|
* Known conservative gap: the plugin's legacy route additionally dedups
|
|
64
155
|
* candidates against the legacy evolution ledger before trimming; the
|
|
@@ -66,14 +157,13 @@ export interface PromptInjectionProjection {
|
|
|
66
157
|
* forecast slightly MORE consumption than the real injection (never less).
|
|
67
158
|
*
|
|
68
159
|
* PR-1894 — round-key alignment. The plugin derives its fair-rotation round
|
|
69
|
-
* key from the CURRENT session's turn ordinal
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* input and exposes `selectionPolicy` on the result.
|
|
160
|
+
* key from the CURRENT session's turn ordinal (`nextSessionTurnOrdinal`),
|
|
161
|
+
* which the console has no access to. Passing no key therefore does NOT make
|
|
162
|
+
* the forecast "more conservative": it selects a DIFFERENT policy
|
|
163
|
+
* (`legacy_fifo_prefix_v1`) than the one production runs. The console must
|
|
164
|
+
* either supply the round key or report the policy honestly, so this
|
|
165
|
+
* function takes the key as an explicit input and exposes
|
|
166
|
+
* `selectionPolicy` on the result.
|
|
77
167
|
*/
|
|
78
168
|
export declare function buildLivePromptInjectionProjection(input: {
|
|
79
169
|
workspaceDir: string;
|
|
@@ -85,4 +175,30 @@ export declare function buildLivePromptInjectionProjection(input: {
|
|
|
85
175
|
* rather than assuming FIFO.
|
|
86
176
|
*/
|
|
87
177
|
roundKey?: number;
|
|
178
|
+
/** AC-01: explicit route binding — 'codex' always shared, 'openclaw' by flag. */
|
|
179
|
+
targetHost?: PromptInjectionTargetHost;
|
|
88
180
|
}): Promise<PromptInjectionProjection>;
|
|
181
|
+
/**
|
|
182
|
+
* AC-01/R-A1: the host-aware entry every console/CLI consumer should use.
|
|
183
|
+
* Binds the route to real host facts (or an explicit request-level target)
|
|
184
|
+
* and returns either one confirmed projection or an honest `unconfirmed`
|
|
185
|
+
* result carrying per-host forecasts — never a guessed route.
|
|
186
|
+
*/
|
|
187
|
+
export type LivePromptInjectionProjectionResolution = {
|
|
188
|
+
status: 'confirmed';
|
|
189
|
+
decision: PromptInjectionRouteDecision;
|
|
190
|
+
projection: PromptInjectionProjection;
|
|
191
|
+
} | {
|
|
192
|
+
status: 'unconfirmed';
|
|
193
|
+
decision: PromptInjectionRouteDecision;
|
|
194
|
+
perHost: readonly {
|
|
195
|
+
hostKind: PromptInjectionTargetHost;
|
|
196
|
+
projection: PromptInjectionProjection;
|
|
197
|
+
}[];
|
|
198
|
+
};
|
|
199
|
+
export declare function resolveLivePromptInjectionProjection(input: {
|
|
200
|
+
workspaceDir: string;
|
|
201
|
+
targetHost?: PromptInjectionTargetHost;
|
|
202
|
+
excludePrincipleIds?: ReadonlySet<string>;
|
|
203
|
+
roundKey?: number;
|
|
204
|
+
}): Promise<LivePromptInjectionProjectionResolution>;
|