@principles/host-runtime 0.7.14 → 0.8.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/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/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,4 +1,6 @@
|
|
|
1
1
|
import { type ActivatedPrinciple, type PromptSelectionPolicy } from '@principles/core/runtime-v2';
|
|
2
|
+
/** PRI-904 SPEC §10: bounded diagnostic id lists (mirrors the core selector's cap of 16). */
|
|
3
|
+
export declare const MAX_SHARED_DIAGNOSTIC_IDS = 16;
|
|
2
4
|
export interface ActivePrinciplePromptResult {
|
|
3
5
|
additionalContext: string;
|
|
4
6
|
principleIds: string[];
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { escapeXml } from '@principles/core/prompt-builder';
|
|
5
5
|
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
6
6
|
/** PRI-904 SPEC §10: bounded diagnostic id lists (mirrors the core selector's cap of 16). */
|
|
7
|
-
const MAX_SHARED_DIAGNOSTIC_IDS = 16;
|
|
7
|
+
export const MAX_SHARED_DIAGNOSTIC_IDS = 16;
|
|
8
8
|
/**
|
|
9
9
|
* PR #1844 follow-up: the activation READ half of buildActivePrinciplePromptContext,
|
|
10
10
|
* exported so the console budget projection can share the exact same input
|
|
@@ -23,6 +23,33 @@ export type PromotionHostLivenessResolution = {
|
|
|
23
23
|
hostRuntimeVersion: null;
|
|
24
24
|
nextAction: string;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Workspace host-kind facts shared by every host-aware projection (prompt
|
|
28
|
+
* injection route resolution reuses the same two durable provenance sources
|
|
29
|
+
* as promotion liveness — one truth, no second registry):
|
|
30
|
+
* - `declaredKinds`: host-authored declarations under
|
|
31
|
+
* `.pd/host-tool-semantics/<hostKind>.json` (each host writes its own file
|
|
32
|
+
* on startup);
|
|
33
|
+
* - `evidenceKinds`: distinct `pain_events.host_kind` values in
|
|
34
|
+
* trajectory.db (which hosts actually produced governance evidence here —
|
|
35
|
+
* survives declaration deletion); pre-column tables carry no evidence by
|
|
36
|
+
* construction;
|
|
37
|
+
* - `effectiveKinds`: union of both, sorted, deduplicated.
|
|
38
|
+
*/
|
|
39
|
+
export type WorkspaceHostKindFacts = {
|
|
40
|
+
readonly ok: true;
|
|
41
|
+
readonly declaredKinds: readonly string[];
|
|
42
|
+
readonly evidenceKinds: readonly string[];
|
|
43
|
+
readonly effectiveKinds: readonly string[];
|
|
44
|
+
} | {
|
|
45
|
+
readonly ok: false;
|
|
46
|
+
readonly reason: 'host_declarations_unreadable' | 'workspace_provenance_unreadable';
|
|
47
|
+
/** Declarations that DID parse before the failure (empty when declarations themselves failed). */
|
|
48
|
+
readonly declaredKinds: readonly string[];
|
|
49
|
+
readonly detail: string;
|
|
50
|
+
readonly nextAction: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function readWorkspaceHostKindFacts(workspaceDir: string): WorkspaceHostKindFacts;
|
|
26
53
|
/**
|
|
27
54
|
* PRI-813: resolve the promotion host-liveness contract from the workspace's
|
|
28
55
|
* REAL host state instead of unconditionally inheriting the OpenClaw
|
|
@@ -72,6 +72,34 @@ function readWorkspaceHostProvenance(workspaceDir) {
|
|
|
72
72
|
function promotionHostFailure(reason, hostKinds, nextAction) {
|
|
73
73
|
return { ok: false, reason, hostKinds, hostContract: null, hostRuntimeVersion: null, nextAction };
|
|
74
74
|
}
|
|
75
|
+
export function readWorkspaceHostKindFacts(workspaceDir) {
|
|
76
|
+
const loaded = loadHostToolDeclarations(workspaceDir);
|
|
77
|
+
if (!loaded.ok && loaded.reason !== 'host_tool_declaration_missing') {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: 'host_declarations_unreadable',
|
|
81
|
+
declaredKinds: [],
|
|
82
|
+
detail: `${loaded.reason} (${loaded.nextAction})`,
|
|
83
|
+
nextAction: `repair the host tool declarations before relying on host facts: ${loaded.reason} (${loaded.nextAction})`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const declaredKinds = loaded.ok
|
|
87
|
+
? [...new Set(loaded.declarations.map((declaration) => declaration.hostKind))].sort()
|
|
88
|
+
: [];
|
|
89
|
+
const provenance = readWorkspaceHostProvenance(workspaceDir);
|
|
90
|
+
if (!provenance.ok) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
reason: 'workspace_provenance_unreadable',
|
|
94
|
+
declaredKinds,
|
|
95
|
+
detail: provenance.nextAction,
|
|
96
|
+
nextAction: provenance.nextAction,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const evidenceKinds = provenance.kinds.filter((kind) => kind === 'openclaw' || kind === 'codex');
|
|
100
|
+
const effectiveKinds = [...new Set([...declaredKinds, ...evidenceKinds])].sort();
|
|
101
|
+
return { ok: true, declaredKinds, evidenceKinds, effectiveKinds };
|
|
102
|
+
}
|
|
75
103
|
/**
|
|
76
104
|
* PRI-813: resolve the promotion host-liveness contract from the workspace's
|
|
77
105
|
* REAL host state instead of unconditionally inheriting the OpenClaw
|
|
@@ -107,19 +135,17 @@ function promotionHostFailure(reason, hostKinds, nextAction) {
|
|
|
107
135
|
* truthfully over state that already exists.
|
|
108
136
|
*/
|
|
109
137
|
export function resolvePromotionHostLiveness(workspaceDir) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!provenance.ok) {
|
|
119
|
-
return promotionHostFailure('workspace_provenance_unreadable', declaredKinds, provenance.nextAction);
|
|
138
|
+
// Same two provenance sources and failure semantics as before, now via the
|
|
139
|
+
// shared facts reader (readWorkspaceHostKindFacts) so prompt-injection
|
|
140
|
+
// route resolution and promotion liveness cannot drift apart.
|
|
141
|
+
const facts = readWorkspaceHostKindFacts(workspaceDir);
|
|
142
|
+
if (!facts.ok) {
|
|
143
|
+
return promotionHostFailure(facts.reason, facts.declaredKinds, facts.reason === 'host_declarations_unreadable'
|
|
144
|
+
? `repair the host tool declarations before promoting: ${facts.detail}`
|
|
145
|
+
: facts.nextAction);
|
|
120
146
|
}
|
|
121
|
-
const
|
|
122
|
-
const effectiveKinds =
|
|
147
|
+
const { declaredKinds } = facts;
|
|
148
|
+
const { effectiveKinds } = facts;
|
|
123
149
|
// The promotion host can only be the OpenClaw contract when nothing on the
|
|
124
150
|
// workspace — neither a declaration nor behavioral evidence — says Codex.
|
|
125
151
|
if (!effectiveKinds.includes('codex')) {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { type PainDatabaseFactory, type PainEnrichmentProvider } from './product
|
|
|
6
6
|
import type { GovernanceHostKind, ToolSemanticRegistry } from '@principles/core/runtime-v2';
|
|
7
7
|
export * from './active-principle-prompt.js';
|
|
8
8
|
export * from './prompt-injection-projection.js';
|
|
9
|
+
export * from './prompt-activation-deliverability.js';
|
|
10
|
+
export * from './prompt-injection-status.js';
|
|
11
|
+
export * from './prompt-injection-event-evidence.js';
|
|
9
12
|
export * from './pd-config.js';
|
|
10
13
|
export * from './production-rulehost-gate.js';
|
|
11
14
|
export * from './rule-implementation-runtime.js';
|
|
@@ -30,6 +33,7 @@ export * from './codex-ingestion-consent.js';
|
|
|
30
33
|
export * from './codex-worker-status.js';
|
|
31
34
|
export * from './codex-legacy-registration.js';
|
|
32
35
|
export * from './codex-transcript-locate.js';
|
|
36
|
+
export * from './intervention-evidence-ingress.js';
|
|
33
37
|
export declare const HOST_RUNTIME_ROUTES: readonly ['before_prompt_build', 'before_tool_call', 'after_tool_call'];
|
|
34
38
|
export type HostRuntimeRoute = (typeof HOST_RUNTIME_ROUTES)[number];
|
|
35
39
|
export type HostRuntimePort = (event: HostEvent) => HostEventResult | Promise<HostEventResult>;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { isHostEvent, isHostEventResult, } from '@principles/core/host';
|
|
2
3
|
import { RUNTIME_V2_PRINCIPLE_BUDGET } from '@principles/core/runtime-v2';
|
|
3
4
|
import { buildActivePrinciplePromptContext } from './active-principle-prompt.js';
|
|
@@ -5,6 +6,15 @@ import { createProductionRuleHostGate } from './production-rulehost-gate.js';
|
|
|
5
6
|
import { createProductionPainEvidenceHandler } from './production-pain-evidence.js';
|
|
6
7
|
export * from './active-principle-prompt.js';
|
|
7
8
|
export * from './prompt-injection-projection.js';
|
|
9
|
+
// SPEC PD_PROMPT_CAPACITY_V1: route-aware single-artifact deliverability
|
|
10
|
+
// precheck (B1 write gate + A candidate fit) and the per-live-activation
|
|
11
|
+
// injection status reader (A activation list). ONE implementation shared by
|
|
12
|
+
// Console and CLI — no per-entry cost logic in either consumer.
|
|
13
|
+
export * from './prompt-activation-deliverability.js';
|
|
14
|
+
export * from './prompt-injection-status.js';
|
|
15
|
+
// SPEC PD_PROMPT_CAPACITY_V1 Phase C (AC-12): honest read-model over the
|
|
16
|
+
// existing injection-event JSONL — unknown vs proven-zero never conflated.
|
|
17
|
+
export * from './prompt-injection-event-evidence.js';
|
|
8
18
|
export * from './pd-config.js';
|
|
9
19
|
export * from './production-rulehost-gate.js';
|
|
10
20
|
export * from './rule-implementation-runtime.js';
|
|
@@ -49,6 +59,10 @@ export * from './codex-legacy-registration.js';
|
|
|
49
59
|
// PRI-625 Slice D: transcript locator moved here so the §15 health service
|
|
50
60
|
// computes per-rollout lag with the SAME locator the catch-up path uses.
|
|
51
61
|
export * from './codex-transcript-locate.js';
|
|
62
|
+
// PD v2 Phase 1: shared intervention evidence ingress — the ONE validated
|
|
63
|
+
// entry point every host source (OpenClaw hooks, Codex pd-hook) uses to
|
|
64
|
+
// append normalized evidence. Never throws; never delays a hook.
|
|
65
|
+
export * from './intervention-evidence-ingress.js';
|
|
52
66
|
export const HOST_RUNTIME_ROUTES = [
|
|
53
67
|
'before_prompt_build',
|
|
54
68
|
'before_tool_call',
|
|
@@ -212,6 +226,8 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
212
226
|
skippedWarnings: prompt.warnings,
|
|
213
227
|
injectedCharCount: prompt.additionalContext.length,
|
|
214
228
|
budget: RUNTIME_V2_PRINCIPLE_BUDGET,
|
|
229
|
+
...(options.hostKind === 'openclaw' || options.hostKind === 'codex' ? { hostKind: options.hostKind } : {}),
|
|
230
|
+
eventId: randomUUID(),
|
|
215
231
|
...(prompt.truncated !== undefined ? { v2Truncated: prompt.truncated } : {}),
|
|
216
232
|
...(event.context.turnId !== undefined ? { runId: event.context.turnId } : {}),
|
|
217
233
|
// PRI-904: selection diagnostics (bounded, optional). On this route
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type InterventionActivationOccurrenceRef, type InterventionContentRef, type InterventionEvidenceBatchInput } from '@principles/core/runtime-v2';
|
|
2
|
+
export interface InterventionIngressAppendResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
insertedCount?: number;
|
|
5
|
+
duplicateCount?: number;
|
|
6
|
+
conflictCount?: number;
|
|
7
|
+
reason?: string;
|
|
8
|
+
nextAction?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExistingInterventionObservationSource {
|
|
11
|
+
sourceKind: 'openclaw_plugin_event_log' | 'codex_pd_hook_event_log';
|
|
12
|
+
sourceLocator: string;
|
|
13
|
+
observationKey: string;
|
|
14
|
+
principleId: string;
|
|
15
|
+
activationId: string;
|
|
16
|
+
artifactId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface InterventionEvidenceIngress {
|
|
19
|
+
/** Best-effort normalized append. Never throws. */
|
|
20
|
+
appendObservationBatch(input: {
|
|
21
|
+
workspaceDir: string;
|
|
22
|
+
batch: InterventionEvidenceBatchInput;
|
|
23
|
+
}): InterventionIngressAppendResult;
|
|
24
|
+
/** Evidence scope id for a governance workspace (stable per workspace). */
|
|
25
|
+
evidenceScopeIdFor(workspaceDir: string): string;
|
|
26
|
+
/** Digest the actual governed artifact content, not activation metadata. */
|
|
27
|
+
resolveInterventionContentRef(workspaceDir: string, activationRef: InterventionActivationOccurrenceRef, principleId: string): InterventionContentRef | null;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the activation occurrence metadata snapshot. The snapshot digest
|
|
30
|
+
* is not a Principle content digest; use resolveInterventionContentRef for
|
|
31
|
+
* the artifact's actual bytes.
|
|
32
|
+
*/
|
|
33
|
+
resolveActivationOccurrenceRef(workspaceDir: string, activationId: string): InterventionActivationOccurrenceRef | null;
|
|
34
|
+
/** Reuse the exact references already persisted for an idempotent source key. */
|
|
35
|
+
existingObservationRefs(workspaceDir: string, source: ExistingInterventionObservationSource): {
|
|
36
|
+
activationRef: InterventionActivationOccurrenceRef;
|
|
37
|
+
contentRef: InterventionContentRef;
|
|
38
|
+
} | null;
|
|
39
|
+
}
|
|
40
|
+
export declare function createInterventionEvidenceIngress(): InterventionEvidenceIngress;
|
|
41
|
+
export declare function getInterventionEvidenceIngress(): InterventionEvidenceIngress;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intervention Evidence Ingress — the single normalized entry point every
|
|
3
|
+
* runtime source uses to append intervention evidence (PD v2 Phase 1,
|
|
4
|
+
* ADR-0027 §2.3; SPEC §13.7 "统一入口").
|
|
5
|
+
*
|
|
6
|
+
* Host adapters build observation batches; ONLY this module validates,
|
|
7
|
+
* normalizes and persists them. No adapter writes evidence tables directly.
|
|
8
|
+
*
|
|
9
|
+
* Failure contract (SPEC §13.7): evidence persistence failure must NEVER
|
|
10
|
+
* change an already computed tool allow/deny. The ingress never throws —
|
|
11
|
+
* every failure returns a structured result with reason + nextAction, and
|
|
12
|
+
* the durable raw source (event log / receipt ledger rows) stays available
|
|
13
|
+
* for bounded replay. The writer connection uses busyTimeoutMs: 0, so a
|
|
14
|
+
* contended state.db lock surfaces immediately as storage_unavailable
|
|
15
|
+
* instead of delaying a hook.
|
|
16
|
+
*
|
|
17
|
+
* Scope identity (SPEC §13.3): evidence_scope_id is persisted in the local
|
|
18
|
+
* evidence ledger and reused after workspace relocation. A canonical path
|
|
19
|
+
* only provides a deterministic candidate when a ledger has no scope yet.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
23
|
+
import { realpathSync } from 'node:fs';
|
|
24
|
+
import { normalizeInterventionEvidenceBatch, SqliteConnection, SqliteInterventionEvidenceStore, } from '@principles/core/runtime-v2';
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
export function createInterventionEvidenceIngress() {
|
|
29
|
+
function scopeIdCandidateFor(workspaceDir) {
|
|
30
|
+
const absolute = isAbsolute(workspaceDir) ? resolve(workspaceDir) : resolve(process.cwd(), workspaceDir);
|
|
31
|
+
// Resolve aliases before using the path as a first-append candidate. Once
|
|
32
|
+
// persisted, the database scope remains authoritative across relocations.
|
|
33
|
+
let resolved = absolute;
|
|
34
|
+
try {
|
|
35
|
+
resolved = realpathSync(absolute);
|
|
36
|
+
}
|
|
37
|
+
catch { /* keep absolute */ }
|
|
38
|
+
return `pd-evidence-scope:sha256:${createHash('sha256').update(resolved, 'utf8').digest('hex').slice(0, 32)}`;
|
|
39
|
+
}
|
|
40
|
+
function evidenceScopeIdFor(workspaceDir) {
|
|
41
|
+
const candidate = scopeIdCandidateFor(workspaceDir);
|
|
42
|
+
let connection = null;
|
|
43
|
+
try {
|
|
44
|
+
connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
45
|
+
const established = new SqliteInterventionEvidenceStore(connection).getEvidenceScope();
|
|
46
|
+
if (established)
|
|
47
|
+
return established.scopeId;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// A missing or unreadable read-only database cannot confirm a persisted
|
|
51
|
+
// scope; return an uncached candidate and let the writer recheck.
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
try {
|
|
55
|
+
connection?.close();
|
|
56
|
+
}
|
|
57
|
+
catch { /* best-effort */ }
|
|
58
|
+
}
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
function resolveInterventionContentRef(workspaceDir, activationRef, principleId) {
|
|
62
|
+
if (!activationRef.artifactId)
|
|
63
|
+
return null;
|
|
64
|
+
let connection = null;
|
|
65
|
+
try {
|
|
66
|
+
connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
67
|
+
const db = connection.getDb();
|
|
68
|
+
// A replay may refer to a deactivated occurrence. Its current artifact
|
|
69
|
+
// bytes cannot prove what was injected at the historical event time.
|
|
70
|
+
// Resolve content only while the exact activation occurrence is live.
|
|
71
|
+
const occurrence = db.prepare(`
|
|
72
|
+
SELECT 1 FROM activations
|
|
73
|
+
WHERE activation_id = ? AND idempotency_key = ? AND deactivated_at IS NULL
|
|
74
|
+
`).get(activationRef.activationId, activationRef.idempotencyKey ?? '');
|
|
75
|
+
if (!occurrence)
|
|
76
|
+
return null;
|
|
77
|
+
const row = db.prepare('SELECT content_json, source_principle_id FROM pi_artifacts WHERE artifact_id = ?').get(activationRef.artifactId);
|
|
78
|
+
if (!isRecord(row)
|
|
79
|
+
|| typeof row.content_json !== 'string'
|
|
80
|
+
|| row.source_principle_id !== principleId)
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
principleId,
|
|
84
|
+
artifactId: activationRef.artifactId,
|
|
85
|
+
payloadDigest: `sha256:${createHash('sha256').update(row.content_json, 'utf8').digest('hex')}`,
|
|
86
|
+
resolution: 'resolved',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
try {
|
|
94
|
+
connection?.close();
|
|
95
|
+
}
|
|
96
|
+
catch { /* best-effort */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function appendObservationBatch(input) {
|
|
100
|
+
const normalization = normalizeInterventionEvidenceBatch(input.batch);
|
|
101
|
+
if (!normalization.ok) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: `evidence_batch_rejected:${normalization.reason}`,
|
|
105
|
+
nextAction: 'Fix the producer payload; nothing was written. The raw source remains durable.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
let connection = null;
|
|
109
|
+
try {
|
|
110
|
+
connection = new SqliteConnection({ workspaceDir: input.workspaceDir, busyTimeoutMs: 0 });
|
|
111
|
+
const store = new SqliteInterventionEvidenceStore(connection);
|
|
112
|
+
const expectedScope = store.getEvidenceScope()?.scopeId ?? scopeIdCandidateFor(input.workspaceDir);
|
|
113
|
+
if (input.batch.evidenceScopeId !== expectedScope) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
reason: `evidence_scope_mismatch:${input.batch.evidenceScopeId}`,
|
|
117
|
+
nextAction: 'Resolve the evidence scope for this workspace and rebuild the batch before replay; no records were written.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const result = store.appendObservationBatch(normalization.batch);
|
|
121
|
+
return {
|
|
122
|
+
ok: result.ok,
|
|
123
|
+
insertedCount: result.insertedCount,
|
|
124
|
+
duplicateCount: result.duplicates.length,
|
|
125
|
+
conflictCount: result.conflicts.length,
|
|
126
|
+
...(result.reason !== undefined ? { reason: result.reason } : {}),
|
|
127
|
+
...(result.nextAction !== undefined ? { nextAction: result.nextAction } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: `evidence_storage_failed:${message}`,
|
|
135
|
+
nextAction: 'The raw source remains durable; replay the same observation batch later — idempotency makes replay safe.',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
try {
|
|
140
|
+
connection?.close();
|
|
141
|
+
}
|
|
142
|
+
catch { /* best-effort */ }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function resolveActivationOccurrenceRef(workspaceDir, activationId) {
|
|
146
|
+
let connection = null;
|
|
147
|
+
try {
|
|
148
|
+
connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
149
|
+
const db = connection.getDb();
|
|
150
|
+
const activationRows = db.prepare(`
|
|
151
|
+
SELECT activation_id, idempotency_key, artifact_id, channel, action, target_ref, activated_at, deactivated_at
|
|
152
|
+
FROM activations
|
|
153
|
+
WHERE activation_id = ?
|
|
154
|
+
ORDER BY activated_at DESC
|
|
155
|
+
`).all(activationId);
|
|
156
|
+
// Without the source's activation-time snapshot, reused ids cannot be
|
|
157
|
+
// matched to one historical row safely. Keep this as an explicit gap.
|
|
158
|
+
if (activationRows.length !== 1)
|
|
159
|
+
return null;
|
|
160
|
+
const activationRow = activationRows[0];
|
|
161
|
+
if (!isRecord(activationRow))
|
|
162
|
+
return null;
|
|
163
|
+
const artifactId = typeof activationRow.artifact_id === 'string' ? activationRow.artifact_id : null;
|
|
164
|
+
let artifactDigest = null;
|
|
165
|
+
// A deactivated activation row proves occurrence metadata, but the
|
|
166
|
+
// artifact table only exposes current content, not an as-activated copy.
|
|
167
|
+
if (artifactId && activationRow.deactivated_at == null) {
|
|
168
|
+
const artifactRow = db.prepare(`
|
|
169
|
+
SELECT artifact_id, artifact_kind, source_task_id, source_principle_id, source_rule_id,
|
|
170
|
+
lineage_artifact_ids, validation_status, content_json, created_at, updated_at
|
|
171
|
+
FROM pi_artifacts WHERE artifact_id = ?
|
|
172
|
+
`).get(artifactId);
|
|
173
|
+
if (isRecord(artifactRow) && typeof artifactRow.content_json === 'string') {
|
|
174
|
+
// Digest the governed artifact bytes exactly as captured — this is
|
|
175
|
+
// the content the approval/activation authority owns (SPEC §13.3).
|
|
176
|
+
artifactDigest = `sha256:${createHash('sha256').update(artifactRow.content_json, 'utf8').digest('hex')}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const snapshotBody = {
|
|
180
|
+
activationId,
|
|
181
|
+
idempotencyKey: activationRow.idempotency_key ?? null,
|
|
182
|
+
artifactId,
|
|
183
|
+
channel: activationRow.channel ?? null,
|
|
184
|
+
action: activationRow.action ?? null,
|
|
185
|
+
activatedAt: activationRow.activated_at ?? null,
|
|
186
|
+
artifactDigest,
|
|
187
|
+
};
|
|
188
|
+
// An activation row can survive after its artifact has been removed.
|
|
189
|
+
// Its metadata snapshot remains useful, but it is not a content digest.
|
|
190
|
+
return {
|
|
191
|
+
activationId,
|
|
192
|
+
...(typeof activationRow.idempotency_key === 'string' ? { idempotencyKey: activationRow.idempotency_key } : {}),
|
|
193
|
+
...(artifactId !== null ? { artifactId } : {}),
|
|
194
|
+
...(typeof activationRow.channel === 'string' ? { channel: activationRow.channel } : {}),
|
|
195
|
+
...(typeof activationRow.activated_at === 'string' ? { activatedAt: activationRow.activated_at } : {}),
|
|
196
|
+
sourceSnapshotDigest: `sha256:${createHash('sha256').update(JSON.stringify(snapshotBody), 'utf8').digest('hex')}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Resolution is best-effort metadata for the evidence reference; an
|
|
201
|
+
// unreadable store leaves the caller to record the honest gap.
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
try {
|
|
206
|
+
connection?.close();
|
|
207
|
+
}
|
|
208
|
+
catch { /* best-effort */ }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function existingObservationRefs(workspaceDir, source) {
|
|
212
|
+
let connection = null;
|
|
213
|
+
try {
|
|
214
|
+
connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
215
|
+
const row = connection.getDb().prepare(`
|
|
216
|
+
SELECT activation_ref_json, content_ref_json
|
|
217
|
+
FROM intervention_evidence_records WHERE source_kind = ? AND source_locator = ? AND observation_key = ?
|
|
218
|
+
`).get(source.sourceKind, source.sourceLocator, source.observationKey);
|
|
219
|
+
if (!isRecord(row) || typeof row.activation_ref_json !== 'string' || typeof row.content_ref_json !== 'string')
|
|
220
|
+
return null;
|
|
221
|
+
const activationRaw = JSON.parse(row.activation_ref_json);
|
|
222
|
+
const contentRaw = JSON.parse(row.content_ref_json);
|
|
223
|
+
const validDigest = (value) => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
224
|
+
if (!isRecord(activationRaw) || typeof activationRaw.activationId !== 'string'
|
|
225
|
+
|| !validDigest(activationRaw.sourceSnapshotDigest)
|
|
226
|
+
|| !isRecord(contentRaw) || typeof contentRaw.principleId !== 'string'
|
|
227
|
+
|| (contentRaw.resolution !== 'resolved' && contentRaw.resolution !== 'revision_reference_unresolved')
|
|
228
|
+
|| (contentRaw.resolution === 'resolved' && !validDigest(contentRaw.payloadDigest))
|
|
229
|
+
|| (contentRaw.resolution === 'revision_reference_unresolved' && contentRaw.payloadDigest !== undefined))
|
|
230
|
+
return null;
|
|
231
|
+
if (activationRaw.activationId !== source.activationId || contentRaw.principleId !== source.principleId
|
|
232
|
+
|| (typeof activationRaw.artifactId === 'string' && contentRaw.artifactId !== activationRaw.artifactId)
|
|
233
|
+
|| (source.artifactId !== undefined && (activationRaw.artifactId !== source.artifactId
|
|
234
|
+
|| contentRaw.artifactId !== source.artifactId)))
|
|
235
|
+
return null;
|
|
236
|
+
const activationRef = {
|
|
237
|
+
activationId: activationRaw.activationId,
|
|
238
|
+
sourceSnapshotDigest: activationRaw.sourceSnapshotDigest,
|
|
239
|
+
...(typeof activationRaw.idempotencyKey === 'string' ? { idempotencyKey: activationRaw.idempotencyKey } : {}),
|
|
240
|
+
...(typeof activationRaw.artifactId === 'string' ? { artifactId: activationRaw.artifactId } : {}),
|
|
241
|
+
...(typeof activationRaw.channel === 'string' ? { channel: activationRaw.channel } : {}),
|
|
242
|
+
...(typeof activationRaw.activatedAt === 'string' ? { activatedAt: activationRaw.activatedAt } : {}),
|
|
243
|
+
};
|
|
244
|
+
const contentRef = {
|
|
245
|
+
principleId: contentRaw.principleId,
|
|
246
|
+
resolution: contentRaw.resolution,
|
|
247
|
+
...(typeof contentRaw.artifactId === 'string' ? { artifactId: contentRaw.artifactId } : {}),
|
|
248
|
+
...(typeof contentRaw.version === 'string' ? { version: contentRaw.version } : {}),
|
|
249
|
+
...(typeof contentRaw.payloadDigest === 'string' ? { payloadDigest: contentRaw.payloadDigest } : {}),
|
|
250
|
+
...(typeof contentRaw.approvalRef === 'string' ? { approvalRef: contentRaw.approvalRef } : {}),
|
|
251
|
+
};
|
|
252
|
+
return { activationRef, contentRef };
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
try {
|
|
259
|
+
connection?.close();
|
|
260
|
+
}
|
|
261
|
+
catch { /* best-effort */ }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { appendObservationBatch, evidenceScopeIdFor, resolveInterventionContentRef, resolveActivationOccurrenceRef, existingObservationRefs };
|
|
265
|
+
}
|
|
266
|
+
/** Process-wide default ingress. */
|
|
267
|
+
let defaultIngress = null;
|
|
268
|
+
export function getInterventionEvidenceIngress() {
|
|
269
|
+
if (!defaultIngress)
|
|
270
|
+
defaultIngress = createInterventionEvidenceIngress();
|
|
271
|
+
return defaultIngress;
|
|
272
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type ActivatedPrinciple } from '@principles/core/runtime-v2';
|
|
2
|
+
import { PROMPT_INJECTION_UNIT, type PromptInjectionBudgetScope, type PromptInjectionRouteDecision, type PromptInjectionRouteName, type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
3
|
+
/**
|
|
4
|
+
* SPEC PD_PROMPT_CAPACITY_V1 R-B2: ONE route-aware single-artifact capacity
|
|
5
|
+
* precheck, shared by every real prompt-activation write entry (Console
|
|
6
|
+
* approve, CLI approve) and by the pre-approval candidate fit (R-A2). The
|
|
7
|
+
* cost is measured with the SAME production serializer/selector the bound
|
|
8
|
+
* route uses — list route bills the selected-lines entry (header + entry),
|
|
9
|
+
* shared route bills the full single-principle directive render including
|
|
10
|
+
* the self-report footer when that flag is on. The principle identity is
|
|
11
|
+
* the REAL resolved id (content `principleId` or draft title) — never a
|
|
12
|
+
* fixed-length UUID allowance (a title-derived id can only be longer).
|
|
13
|
+
*
|
|
14
|
+
* This module deliberately does NOT read host configs from core: it lives
|
|
15
|
+
* in host-runtime and consumes the workspace config + host facts here.
|
|
16
|
+
*/
|
|
17
|
+
export interface PromptDeliverabilityCandidateArtifact {
|
|
18
|
+
artifactId: string;
|
|
19
|
+
artifactKind: string;
|
|
20
|
+
contentJson: string;
|
|
21
|
+
validationStatus: string;
|
|
22
|
+
}
|
|
23
|
+
export type PromptArtifactDeliverability = {
|
|
24
|
+
status: 'deliverable';
|
|
25
|
+
route: PromptInjectionRouteName;
|
|
26
|
+
hostKind?: PromptInjectionTargetHost;
|
|
27
|
+
basis?: PromptInjectionRouteDecision['basis'];
|
|
28
|
+
budget: number;
|
|
29
|
+
/** Single-item cost in UTF-16 code units under the bound route's serializer. */
|
|
30
|
+
costChars: number;
|
|
31
|
+
budgetScope: PromptInjectionBudgetScope;
|
|
32
|
+
unit: typeof PROMPT_INJECTION_UNIT;
|
|
33
|
+
} | {
|
|
34
|
+
status: 'undeliverable';
|
|
35
|
+
reason: 'single_item_exceeds_budget';
|
|
36
|
+
route: PromptInjectionRouteName;
|
|
37
|
+
hostKind?: PromptInjectionTargetHost;
|
|
38
|
+
basis?: PromptInjectionRouteDecision['basis'];
|
|
39
|
+
budget: number;
|
|
40
|
+
costChars: number;
|
|
41
|
+
overByChars: number;
|
|
42
|
+
budgetScope: PromptInjectionBudgetScope;
|
|
43
|
+
unit: typeof PROMPT_INJECTION_UNIT;
|
|
44
|
+
nextAction: string;
|
|
45
|
+
} | {
|
|
46
|
+
status: 'unconfirmed';
|
|
47
|
+
/** Stable machine-readable reason (route facts or artifact content unresolvable). */
|
|
48
|
+
reason: string;
|
|
49
|
+
nextAction: string;
|
|
50
|
+
/** Route-unconfirmed case: per-host single-item fit so the UI can show both. */
|
|
51
|
+
perHost?: readonly {
|
|
52
|
+
hostKind: PromptInjectionTargetHost;
|
|
53
|
+
fits: boolean;
|
|
54
|
+
costChars: number;
|
|
55
|
+
}[];
|
|
56
|
+
};
|
|
57
|
+
/** Resolve the REAL principle identity + text from the artifact content. */
|
|
58
|
+
export declare function resolvePromptCandidatePrinciple(artifact: PromptDeliverabilityCandidateArtifact): {
|
|
59
|
+
ok: true;
|
|
60
|
+
principle: ActivatedPrinciple;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
warning: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Positive single-item evidence under a given route, using the production
|
|
67
|
+
* selector itself (AC-06: a row is "fits" because its own serialized entry
|
|
68
|
+
* fits — never because it is absent from a bounded diagnostic list).
|
|
69
|
+
*
|
|
70
|
+
* `costChars` is what the single-entry selection would consume, measured the
|
|
71
|
+
* same way the route's projection reports `usedChars` (the joined selected
|
|
72
|
+
* lines). For an entry that does NOT fit, trimToBudget returns only the
|
|
73
|
+
* header line, so the would-be cost is reconstructed from the header line it
|
|
74
|
+
* DID return plus the real entry line (same format authority, listEntryLine).
|
|
75
|
+
*/
|
|
76
|
+
export declare function computePromptSingleItemFit(input: {
|
|
77
|
+
principle: ActivatedPrinciple;
|
|
78
|
+
route: PromptInjectionRouteName;
|
|
79
|
+
selfReportEnabled: boolean;
|
|
80
|
+
}): {
|
|
81
|
+
fits: boolean;
|
|
82
|
+
costChars: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Precheck one artifact against the workspace's effective injection route.
|
|
86
|
+
* Pure read — no state mutation, safe to call before any governance write.
|
|
87
|
+
*/
|
|
88
|
+
export declare function checkPromptArtifactDeliverability(input: {
|
|
89
|
+
workspaceDir: string;
|
|
90
|
+
targetHost?: PromptInjectionTargetHost;
|
|
91
|
+
artifact: PromptDeliverabilityCandidateArtifact | null | undefined;
|
|
92
|
+
}): Promise<PromptArtifactDeliverability>;
|
|
93
|
+
/** Load one artifact read-only from `<workspace>/.pd/state.db`. Failures carry a reason (rc-9). */
|
|
94
|
+
export declare function loadPromptArtifactById(workspaceDir: string, artifactId: string): Promise<{
|
|
95
|
+
ok: true;
|
|
96
|
+
artifact: PromptDeliverabilityCandidateArtifact;
|
|
97
|
+
} | {
|
|
98
|
+
ok: false;
|
|
99
|
+
reason: string;
|
|
100
|
+
}>;
|
|
101
|
+
/** Combined load + precheck for write gates that only hold an artifact id (CLI approve). */
|
|
102
|
+
export declare function checkPromptArtifactDeliverabilityById(input: {
|
|
103
|
+
workspaceDir: string;
|
|
104
|
+
artifactId: string;
|
|
105
|
+
targetHost?: PromptInjectionTargetHost;
|
|
106
|
+
}): Promise<PromptArtifactDeliverability>;
|