@principles/host-runtime 0.12.1 → 0.13.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/codex-artificer-trusted-facts.d.ts +7 -0
- package/dist/codex-artificer-trusted-facts.js +17 -0
- package/dist/codex-rule-context-provider.js +82 -13
- package/dist/design-gate-facts.d.ts +37 -0
- package/dist/design-gate-facts.js +249 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -1
- package/dist/production-rulehost-gate.js +37 -1
- package/dist/prompt-injection-budget-observation.d.ts +10 -1
- package/dist/prompt-injection-budget-observation.js +21 -2
- package/dist/workspace-behavior-evidence.d.ts +9 -0
- package/dist/workspace-behavior-evidence.js +85 -0
- package/package.json +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ArtificerTrustedFactsPort } from '@principles/core/runtime-v2';
|
|
2
|
+
/**
|
|
3
|
+
* Codex generation can author rules against the same DesignGateFact schema
|
|
4
|
+
* that the native PreToolUse provider supplies. This declares capability only;
|
|
5
|
+
* it never resolves the current generated PI task as approved.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createCodexArtificerTrustedFactsPort(): ArtificerTrustedFactsPort;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describeCodexRuleContextFact } from './design-gate-facts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Codex generation can author rules against the same DesignGateFact schema
|
|
4
|
+
* that the native PreToolUse provider supplies. This declares capability only;
|
|
5
|
+
* it never resolves the current generated PI task as approved.
|
|
6
|
+
*/
|
|
7
|
+
export function createCodexArtificerTrustedFactsPort() {
|
|
8
|
+
return {
|
|
9
|
+
resolveRequiredFact: ({ factKind }) => ({
|
|
10
|
+
resolution: 'unknown',
|
|
11
|
+
detail: factKind === 'design_approval'
|
|
12
|
+
? 'Generation verifies host fact capability; current task approval is resolved only at native PreToolUse.'
|
|
13
|
+
: `Codex has no trusted source for required fact '${factKind}'.`,
|
|
14
|
+
}),
|
|
15
|
+
describeRequiredFact: describeCodexRuleContextFact,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -2,19 +2,57 @@ import path from 'node:path';
|
|
|
2
2
|
import { computeBehaviorFacts, } from '@principles/core/runtime-v2';
|
|
3
3
|
import { assembleBehaviorHistory } from './behavior-history.js';
|
|
4
4
|
import { readWorkspaceRuleContextRowsBefore } from './workspace-behavior-evidence.js';
|
|
5
|
+
import { readLatestCodexDesignGateDeclaration, resolveCodexDesignGateFact } from './design-gate-facts.js';
|
|
5
6
|
const HISTORY_LIMIT = 100;
|
|
6
7
|
const MAX_REASON_LENGTH = 240;
|
|
8
|
+
const MAX_PATCH_BYTES = 1024 * 1024;
|
|
9
|
+
const APPROVED_READONLY_COMMANDS = new Set([
|
|
10
|
+
'git status',
|
|
11
|
+
'git status --short',
|
|
12
|
+
'git diff --stat',
|
|
13
|
+
'git log -1 --oneline',
|
|
14
|
+
]);
|
|
7
15
|
function isRecord(value) {
|
|
8
16
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
17
|
}
|
|
10
|
-
function
|
|
18
|
+
function patchTargets(params, projectDir) {
|
|
19
|
+
if (params === null)
|
|
20
|
+
return undefined;
|
|
21
|
+
const patch = typeof params.patch === 'string'
|
|
22
|
+
? params.patch
|
|
23
|
+
: typeof params.input === 'string' ? params.input : null;
|
|
24
|
+
if (patch === null || Buffer.byteLength(patch, 'utf8') > MAX_PATCH_BYTES)
|
|
25
|
+
return undefined;
|
|
26
|
+
const targets = [];
|
|
27
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
28
|
+
const match = /^\*\*\* (?:Add|Update|Delete) File: (.+)$|^\*\*\* Move (?:from|to): (.+)$/.exec(line);
|
|
29
|
+
if (!match)
|
|
30
|
+
continue;
|
|
31
|
+
const value = (match[1] ?? match[2] ?? '').trim();
|
|
32
|
+
if (value.length === 0 || value.includes('\0'))
|
|
33
|
+
return undefined;
|
|
34
|
+
targets.push(path.resolve(projectDir, value));
|
|
35
|
+
}
|
|
36
|
+
return targets.length > 0 ? targets : undefined;
|
|
37
|
+
}
|
|
38
|
+
function safeReadonlyCommand(params) {
|
|
39
|
+
if (params === null)
|
|
40
|
+
return false;
|
|
41
|
+
const command = typeof params.command === 'string' ? params.command : null;
|
|
42
|
+
return command !== null && APPROVED_READONLY_COMMANDS.has(command.trim());
|
|
43
|
+
}
|
|
44
|
+
function unavailable(reason, designGate) {
|
|
11
45
|
const history = {
|
|
12
46
|
status: 'unavailable',
|
|
13
47
|
unavailableReason: reason.replace(/\s+/g, ' ').slice(0, MAX_REASON_LENGTH),
|
|
14
48
|
truncated: false,
|
|
15
49
|
calls: [],
|
|
16
50
|
};
|
|
17
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
version: 2,
|
|
53
|
+
history,
|
|
54
|
+
facts: { ...computeBehaviorFacts(history, null, null), ...(designGate !== undefined ? { designGate } : {}) },
|
|
55
|
+
};
|
|
18
56
|
}
|
|
19
57
|
/**
|
|
20
58
|
* Read a bounded same-session trajectory window for Codex PreToolUse events.
|
|
@@ -27,24 +65,55 @@ export function createCodexRuleContextProvider(options) {
|
|
|
27
65
|
const workspaceDir = path.resolve(options.workspaceDir);
|
|
28
66
|
const projectDir = path.resolve(options.projectDir);
|
|
29
67
|
return (request) => {
|
|
68
|
+
const canonicalKind = options.toolSemantics.resolve(request.toolName);
|
|
69
|
+
const targetKind = canonicalKind === 'agent' ? 'other' : canonicalKind;
|
|
70
|
+
const raw = request.rawPayload;
|
|
71
|
+
const rawParams = isRecord(raw) && isRecord(raw.toolInput) && isRecord(raw.toolInput.params)
|
|
72
|
+
? raw.toolInput.params
|
|
73
|
+
: null;
|
|
74
|
+
const readonlyExecute = targetKind === 'execute' && safeReadonlyCommand(rawParams);
|
|
75
|
+
// Fail fast before the fact resolution below: a non-string or empty
|
|
76
|
+
// sessionId cannot bind a declaration (an empty id resolves the fact to
|
|
77
|
+
// unknown) and would otherwise throw inside resolveCodexDesignGateFact,
|
|
78
|
+
// surfacing as a provider_failed fault instead of the precise reason.
|
|
79
|
+
// The enabled and workspace-scope checks deliberately stay AFTER
|
|
80
|
+
// resolution: the design-gate fact is independent of the
|
|
81
|
+
// rulecode_context_v2 capability (it reads the governance declaration
|
|
82
|
+
// store directly), so those degraded paths must still carry the resolved
|
|
83
|
+
// fact for enforcement (SPEC-RULECODE-CODEX-LOCAL-CLOSURE-001 §6).
|
|
84
|
+
if (typeof request.sessionId !== 'string' || request.sessionId.trim().length === 0) {
|
|
85
|
+
return unavailable('session_id_invalid');
|
|
86
|
+
}
|
|
87
|
+
const designGate = resolveCodexDesignGateFact({
|
|
88
|
+
governanceWorkspacePath: workspaceDir,
|
|
89
|
+
projectWorkspacePath: projectDir,
|
|
90
|
+
hostSessionId: request.sessionId,
|
|
91
|
+
targetPath: targetKind === 'execute' ? projectDir : request.targetPath,
|
|
92
|
+
...(targetKind === 'write' && request.toolName === 'apply_patch'
|
|
93
|
+
? { targetPaths: patchTargets(rawParams, projectDir) ?? [] }
|
|
94
|
+
: {}),
|
|
95
|
+
...(targetKind === 'execute' ? { projectScopedExecution: true } : {}),
|
|
96
|
+
toolKind: readonlyExecute ? 'read' : targetKind,
|
|
97
|
+
}, {
|
|
98
|
+
getLatestDeclaration(projectWorkspacePath, hostSessionId) {
|
|
99
|
+
return readLatestCodexDesignGateDeclaration(workspaceDir, projectWorkspacePath, hostSessionId);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
30
102
|
if (!options.enabled)
|
|
31
|
-
return unavailable('codex_rule_context_disabled');
|
|
103
|
+
return unavailable('codex_rule_context_disabled', designGate);
|
|
32
104
|
if (path.resolve(request.workspaceDir) !== workspaceDir)
|
|
33
|
-
return unavailable('workspace_scope_mismatch');
|
|
34
|
-
if (typeof request.sessionId !== 'string' || request.sessionId.trim().length === 0)
|
|
35
|
-
return unavailable('session_id_invalid');
|
|
36
|
-
const raw = request.rawPayload;
|
|
105
|
+
return unavailable('workspace_scope_mismatch', designGate);
|
|
37
106
|
const toolInput = isRecord(raw) && isRecord(raw.toolInput) ? raw.toolInput : null;
|
|
38
107
|
if (toolInput === null
|
|
39
108
|
|| toolInput.toolName !== request.toolName
|
|
40
109
|
|| !isRecord(toolInput.params)
|
|
41
110
|
|| !path.isAbsolute(projectDir)
|
|
42
111
|
|| options.currentToolUseId.trim().length === 0) {
|
|
43
|
-
return unavailable('codex_pre_tool_scope_mismatch');
|
|
112
|
+
return unavailable('codex_pre_tool_scope_mismatch', designGate);
|
|
44
113
|
}
|
|
45
114
|
const cutoff = options.cutoffAt;
|
|
46
115
|
if (!(cutoff instanceof Date) || !Number.isFinite(cutoff.getTime()))
|
|
47
|
-
return unavailable('history_cutoff_invalid');
|
|
116
|
+
return unavailable('history_cutoff_invalid', designGate);
|
|
48
117
|
try {
|
|
49
118
|
const historyRows = readWorkspaceRuleContextRowsBefore(workspaceDir, {
|
|
50
119
|
sessionId: request.sessionId,
|
|
@@ -54,9 +123,9 @@ export function createCodexRuleContextProvider(options) {
|
|
|
54
123
|
limit: HISTORY_LIMIT,
|
|
55
124
|
});
|
|
56
125
|
if (historyRows.unscopedPriorRows)
|
|
57
|
-
return unavailable('trajectory_project_scope_incomplete');
|
|
126
|
+
return unavailable('trajectory_project_scope_incomplete', designGate);
|
|
58
127
|
if (historyRows.rows.length === 0)
|
|
59
|
-
return unavailable('no_prior_session_history');
|
|
128
|
+
return unavailable('no_prior_session_history', designGate);
|
|
60
129
|
const history = assembleBehaviorHistory(historyRows.rows, historyRows.truncated, {
|
|
61
130
|
projectDir,
|
|
62
131
|
toolSemantics: options.toolSemantics,
|
|
@@ -79,11 +148,11 @@ export function createCodexRuleContextProvider(options) {
|
|
|
79
148
|
return {
|
|
80
149
|
version: 2,
|
|
81
150
|
history,
|
|
82
|
-
facts,
|
|
151
|
+
facts: { ...facts, designGate },
|
|
83
152
|
};
|
|
84
153
|
}
|
|
85
154
|
catch (error) {
|
|
86
|
-
return unavailable(`trajectory_history_unavailable:${error instanceof Error ? error.message : String(error)}
|
|
155
|
+
return unavailable(`trajectory_history_unavailable:${error instanceof Error ? error.message : String(error)}`, designGate);
|
|
87
156
|
}
|
|
88
157
|
};
|
|
89
158
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type DesignGateFact } from '@principles/core/runtime-v2';
|
|
2
|
+
export declare const DESIGN_GATE_FACT_KIND: 'design_approval';
|
|
3
|
+
export declare const DESIGN_GATE_FACT_INPUT_PATH: 'input.context.facts.designGate';
|
|
4
|
+
export declare const CODEX_DESIGN_GATE_SOURCE: 'codex-native-pretool-design-gate-v1';
|
|
5
|
+
export interface DesignGateSpecSnapshot {
|
|
6
|
+
realPath: string;
|
|
7
|
+
digest: string;
|
|
8
|
+
byteLength: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CodexDesignGateRequest {
|
|
11
|
+
governanceWorkspacePath: string;
|
|
12
|
+
projectWorkspacePath: string;
|
|
13
|
+
hostSessionId: string;
|
|
14
|
+
targetPath: string;
|
|
15
|
+
/** All paths named by a bounded native patch payload, when available. */
|
|
16
|
+
targetPaths?: readonly string[];
|
|
17
|
+
/** True only when the Owner explicitly scoped execute invocations to the project root. */
|
|
18
|
+
projectScopedExecution?: boolean;
|
|
19
|
+
toolKind: 'read' | 'search' | 'write' | 'execute' | 'other';
|
|
20
|
+
}
|
|
21
|
+
export interface DesignGateDeclarationReader {
|
|
22
|
+
getLatestDeclaration(projectWorkspacePath: string, hostSessionId: string): unknown;
|
|
23
|
+
}
|
|
24
|
+
/** Read and hash the current SPEC after realpath confinement on every call. */
|
|
25
|
+
export declare function readDesignGateSpecSnapshot(projectWorkspacePath: string, specPath: string): DesignGateSpecSnapshot;
|
|
26
|
+
/** Resolve a live Codex event against the latest Owner declaration, without caching or writes. */
|
|
27
|
+
export declare function resolveCodexDesignGateFact(request: CodexDesignGateRequest, declarationReader: DesignGateDeclarationReader): DesignGateFact;
|
|
28
|
+
/** Read the current declaration from the governance workspace's existing state.db. */
|
|
29
|
+
export declare function readLatestCodexDesignGateDeclaration(governanceWorkspacePath: string, projectWorkspacePath: string, hostSessionId: string): unknown;
|
|
30
|
+
export declare function describeCodexRuleContextFact(input: {
|
|
31
|
+
factKind: string;
|
|
32
|
+
}): {
|
|
33
|
+
supported: boolean;
|
|
34
|
+
schemaVersion?: string;
|
|
35
|
+
inputPath?: string;
|
|
36
|
+
source?: string;
|
|
37
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DESIGN_GATE_FACT_SCHEMA_VERSION, SqliteConnection, SqliteDesignGateStore, isDesignGateDeclaration, } from '@principles/core/runtime-v2';
|
|
5
|
+
export const DESIGN_GATE_FACT_KIND = 'design_approval';
|
|
6
|
+
export const DESIGN_GATE_FACT_INPUT_PATH = 'input.context.facts.designGate';
|
|
7
|
+
export const CODEX_DESIGN_GATE_SOURCE = 'codex-native-pretool-design-gate-v1';
|
|
8
|
+
const MAX_SPEC_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
function inside(parent, child) {
|
|
10
|
+
const relative = path.relative(parent, child);
|
|
11
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
12
|
+
}
|
|
13
|
+
function realPathForTarget(candidate) {
|
|
14
|
+
if (!path.isAbsolute(candidate))
|
|
15
|
+
throw new Error('target_path_not_absolute');
|
|
16
|
+
let current = path.resolve(candidate);
|
|
17
|
+
const suffix = [];
|
|
18
|
+
while (true) {
|
|
19
|
+
try {
|
|
20
|
+
const realCurrent = fs.realpathSync.native(current);
|
|
21
|
+
return path.join(realCurrent, ...suffix.reverse());
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
const parent = path.dirname(current);
|
|
25
|
+
if (parent === current)
|
|
26
|
+
throw new Error('target_path_unresolvable');
|
|
27
|
+
suffix.push(path.basename(current));
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Read and hash the current SPEC after realpath confinement on every call. */
|
|
33
|
+
export function readDesignGateSpecSnapshot(projectWorkspacePath, specPath) {
|
|
34
|
+
if (!path.isAbsolute(projectWorkspacePath) || !path.isAbsolute(specPath))
|
|
35
|
+
throw new Error('spec_path_invalid');
|
|
36
|
+
const projectRoot = fs.realpathSync.native(projectWorkspacePath);
|
|
37
|
+
const realPath = fs.realpathSync.native(specPath);
|
|
38
|
+
if (!inside(projectRoot, realPath))
|
|
39
|
+
throw new Error('spec_path_invalid');
|
|
40
|
+
// Open once and validate the descriptor: the checked and the read bytes are
|
|
41
|
+
// guaranteed to be the same file (no path-recheck race between stat and read).
|
|
42
|
+
const fd = fs.openSync(realPath, 'r');
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.fstatSync(fd);
|
|
45
|
+
if (!stat.isFile() || stat.size > MAX_SPEC_BYTES)
|
|
46
|
+
throw new Error('spec_path_invalid');
|
|
47
|
+
const bytes = fs.readFileSync(fd);
|
|
48
|
+
if (bytes.byteLength > MAX_SPEC_BYTES)
|
|
49
|
+
throw new Error('spec_path_invalid');
|
|
50
|
+
return {
|
|
51
|
+
realPath,
|
|
52
|
+
digest: createHash('sha256').update(bytes).digest('hex'),
|
|
53
|
+
byteLength: bytes.byteLength,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs.closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function scopeReference(paths, actions) {
|
|
61
|
+
const canonical = JSON.stringify({
|
|
62
|
+
paths: [...paths].sort((left, right) => left.localeCompare(right)),
|
|
63
|
+
actions: [...actions].sort((left, right) => left.localeCompare(right)),
|
|
64
|
+
});
|
|
65
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
66
|
+
}
|
|
67
|
+
function unknownFact(taskRef, reason, requiresApproval = true) {
|
|
68
|
+
return {
|
|
69
|
+
version: 1,
|
|
70
|
+
sourceRef: null,
|
|
71
|
+
revision: null,
|
|
72
|
+
requiresApproval,
|
|
73
|
+
taskRef,
|
|
74
|
+
specDigest: null,
|
|
75
|
+
scopeRef: null,
|
|
76
|
+
decisionTaskRef: null,
|
|
77
|
+
decisionSpecDigest: null,
|
|
78
|
+
decisionScopeRef: null,
|
|
79
|
+
resolution: 'unknown',
|
|
80
|
+
revoked: false,
|
|
81
|
+
invalidationReason: reason,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function targetIsInDeclaredScope(projectPath, targetPath, scopePaths) {
|
|
85
|
+
try {
|
|
86
|
+
const target = realPathForTarget(targetPath);
|
|
87
|
+
const projectRoot = fs.realpathSync.native(projectPath);
|
|
88
|
+
if (!inside(projectRoot, target))
|
|
89
|
+
return null;
|
|
90
|
+
for (const declaredPath of scopePaths) {
|
|
91
|
+
const resolved = path.isAbsolute(declaredPath) ? declaredPath : path.resolve(projectRoot, declaredPath);
|
|
92
|
+
const scopePath = realPathForTarget(resolved);
|
|
93
|
+
if (!inside(projectRoot, scopePath))
|
|
94
|
+
continue;
|
|
95
|
+
let directory = false;
|
|
96
|
+
try {
|
|
97
|
+
directory = fs.statSync(scopePath).isDirectory();
|
|
98
|
+
}
|
|
99
|
+
catch { /* a new file may be an exact scope */ }
|
|
100
|
+
if (target === scopePath || (directory && inside(scopePath, target)))
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function everyTargetIsInDeclaredScope(projectPath, targetPaths, scopePaths) {
|
|
110
|
+
if (targetPaths.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
for (const targetPath of targetPaths) {
|
|
113
|
+
if (!path.isAbsolute(targetPath))
|
|
114
|
+
return null;
|
|
115
|
+
const matches = targetIsInDeclaredScope(projectPath, targetPath, scopePaths);
|
|
116
|
+
if (matches !== true)
|
|
117
|
+
return matches;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/** Resolve a live Codex event against the latest Owner declaration, without caching or writes. */
|
|
122
|
+
export function resolveCodexDesignGateFact(request, declarationReader) {
|
|
123
|
+
const taskRef = request.hostSessionId.trim().length > 0 ? `codex:${request.hostSessionId}` : null;
|
|
124
|
+
if (taskRef === null)
|
|
125
|
+
return unknownFact(null, 'host_identity_unavailable');
|
|
126
|
+
if (!path.isAbsolute(request.governanceWorkspacePath) || !path.isAbsolute(request.projectWorkspacePath)) {
|
|
127
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
128
|
+
}
|
|
129
|
+
// Codex's declared read/search tool semantics are sufficient evidence that
|
|
130
|
+
// the current action is investigative and does not require design approval.
|
|
131
|
+
if (request.toolKind === 'read' || request.toolKind === 'search') {
|
|
132
|
+
return {
|
|
133
|
+
version: 1,
|
|
134
|
+
sourceRef: null,
|
|
135
|
+
revision: null,
|
|
136
|
+
requiresApproval: false,
|
|
137
|
+
taskRef,
|
|
138
|
+
specDigest: null,
|
|
139
|
+
scopeRef: null,
|
|
140
|
+
decisionTaskRef: null,
|
|
141
|
+
decisionSpecDigest: null,
|
|
142
|
+
decisionScopeRef: null,
|
|
143
|
+
resolution: 'not_required',
|
|
144
|
+
revoked: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (request.toolKind !== 'write' && request.toolKind !== 'execute') {
|
|
148
|
+
return unknownFact(taskRef, 'action_scope_unknown');
|
|
149
|
+
}
|
|
150
|
+
// An execute declaration scoped to `.` governs invocations from this native
|
|
151
|
+
// Codex project/session. It does not claim to confine the process effects.
|
|
152
|
+
let projectRoot;
|
|
153
|
+
let governanceRoot;
|
|
154
|
+
try {
|
|
155
|
+
projectRoot = fs.realpathSync.native(request.projectWorkspacePath);
|
|
156
|
+
governanceRoot = fs.realpathSync.native(request.governanceWorkspacePath);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
160
|
+
}
|
|
161
|
+
let declarationValue;
|
|
162
|
+
try {
|
|
163
|
+
declarationValue = declarationReader.getLatestDeclaration(projectRoot, request.hostSessionId);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return unknownFact(taskRef, 'declaration_unavailable');
|
|
167
|
+
}
|
|
168
|
+
if (declarationValue === null)
|
|
169
|
+
return unknownFact(taskRef, 'unregistered_scope');
|
|
170
|
+
if (!isDesignGateDeclaration(declarationValue))
|
|
171
|
+
return unknownFact(taskRef, 'declaration_invalid');
|
|
172
|
+
const declaration = declarationValue;
|
|
173
|
+
try {
|
|
174
|
+
if (fs.realpathSync.native(declaration.projectWorkspacePath) !== projectRoot
|
|
175
|
+
|| fs.realpathSync.native(declaration.governanceWorkspacePath) !== governanceRoot
|
|
176
|
+
|| declaration.hostSessionId !== request.hostSessionId
|
|
177
|
+
|| declaration.taskRef !== taskRef) {
|
|
178
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
183
|
+
}
|
|
184
|
+
const action = request.toolKind;
|
|
185
|
+
if (!declaration.scope.actions.includes(action))
|
|
186
|
+
return unknownFact(taskRef, 'action_scope_unknown');
|
|
187
|
+
const projectScopedExecution = request.toolKind === 'execute'
|
|
188
|
+
&& request.projectScopedExecution === true
|
|
189
|
+
&& declaration.scope.paths.includes('.');
|
|
190
|
+
const targetScope = projectScopedExecution
|
|
191
|
+
? true
|
|
192
|
+
: request.targetPaths !== undefined
|
|
193
|
+
? everyTargetIsInDeclaredScope(projectRoot, request.targetPaths, declaration.scope.paths)
|
|
194
|
+
: request.toolKind === 'execute'
|
|
195
|
+
? null
|
|
196
|
+
: targetIsInDeclaredScope(projectRoot, request.targetPath, declaration.scope.paths);
|
|
197
|
+
if (targetScope !== true)
|
|
198
|
+
return unknownFact(taskRef, 'target_scope_unknown');
|
|
199
|
+
let snapshot;
|
|
200
|
+
try {
|
|
201
|
+
snapshot = readDesignGateSpecSnapshot(projectRoot, declaration.specPath);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return unknownFact(taskRef, 'spec_path_invalid');
|
|
205
|
+
}
|
|
206
|
+
if (snapshot.digest !== declaration.specDigest)
|
|
207
|
+
return unknownFact(taskRef, 'spec_digest_mismatch');
|
|
208
|
+
const currentScopeRef = scopeReference(declaration.scope.paths, declaration.scope.actions);
|
|
209
|
+
const resolution = declaration.revoked ? 'not_approved' : declaration.decision;
|
|
210
|
+
return {
|
|
211
|
+
version: 1,
|
|
212
|
+
sourceRef: declaration.sourceRef,
|
|
213
|
+
revision: declaration.revision,
|
|
214
|
+
requiresApproval: declaration.revoked || declaration.decision !== 'not_required',
|
|
215
|
+
taskRef,
|
|
216
|
+
specDigest: snapshot.digest,
|
|
217
|
+
scopeRef: currentScopeRef,
|
|
218
|
+
decisionTaskRef: declaration.taskRef,
|
|
219
|
+
decisionSpecDigest: declaration.specDigest,
|
|
220
|
+
decisionScopeRef: currentScopeRef,
|
|
221
|
+
resolution,
|
|
222
|
+
revoked: declaration.revoked,
|
|
223
|
+
...(declaration.revoked ? { invalidationReason: 'declaration_revoked' } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Read the current declaration from the governance workspace's existing state.db. */
|
|
227
|
+
export function readLatestCodexDesignGateDeclaration(governanceWorkspacePath, projectWorkspacePath, hostSessionId) {
|
|
228
|
+
const connection = new SqliteConnection({
|
|
229
|
+
workspaceDir: governanceWorkspacePath,
|
|
230
|
+
readonly: true,
|
|
231
|
+
bootstrapIfMissing: false,
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
return new SqliteDesignGateStore(connection).getLatestDeclaration(projectWorkspacePath, hostSessionId);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
connection.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export function describeCodexRuleContextFact(input) {
|
|
241
|
+
if (input.factKind !== DESIGN_GATE_FACT_KIND)
|
|
242
|
+
return { supported: false };
|
|
243
|
+
return {
|
|
244
|
+
supported: DESIGN_GATE_FACT_SCHEMA_VERSION === '1',
|
|
245
|
+
schemaVersion: DESIGN_GATE_FACT_SCHEMA_VERSION,
|
|
246
|
+
inputPath: DESIGN_GATE_FACT_INPUT_PATH,
|
|
247
|
+
source: CODEX_DESIGN_GATE_SOURCE,
|
|
248
|
+
};
|
|
249
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -91,3 +91,6 @@ export * from './behavior-history.js';
|
|
|
91
91
|
export * from './codex-rule-context-provider.js';
|
|
92
92
|
export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
|
|
93
93
|
export * from './explicit-owner-statement-prefix.js';
|
|
94
|
+
export { listObservedDesignGateSessions } from './workspace-behavior-evidence.js';
|
|
95
|
+
export { readDesignGateSpecSnapshot, describeCodexRuleContextFact } from './design-gate-facts.js';
|
|
96
|
+
export { createCodexArtificerTrustedFactsPort } from './codex-artificer-trusted-facts.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isHostEvent, isHostEventResult, } from '@principles/core/host';
|
|
3
|
-
import { getPromptModelCatalog, resolvePromptBudget } from '@principles/core/runtime-v2';
|
|
3
|
+
import { getPromptModelCatalog, operatorDeclaredContextWindow, resolvePromptBudget } from '@principles/core/runtime-v2';
|
|
4
|
+
import { loadPdConfigForPlugin } from './pd-config.js';
|
|
4
5
|
import { buildActivePrinciplePromptContext } from './active-principle-prompt.js';
|
|
5
6
|
import { createProductionRuleHostGate } from './production-rulehost-gate.js';
|
|
6
7
|
import { createProductionPainEvidenceHandler } from './production-pain-evidence.js';
|
|
@@ -212,6 +213,19 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
212
213
|
// reports `legacy_fifo_prefix_v1` honestly. The selector's fair-rotation
|
|
213
214
|
// capability is retained and stays reachable for callers that DO hold a
|
|
214
215
|
// legitimate advancing round authority.
|
|
216
|
+
// PRI-1020: the Owner's declared window (.pd/config.yaml promptBudget)
|
|
217
|
+
// is tier 0 of the budget chain — resolved per host and fed into the
|
|
218
|
+
// SAME single resolver below. One extra sync config read per prompt
|
|
219
|
+
// build (the candidates reader loads it independently): deliberate, so
|
|
220
|
+
// a config flip applies on the next turn with no cache to invalidate.
|
|
221
|
+
// Malformed config degrades to defaults (no declaration) and is already
|
|
222
|
+
// surfaced as config_invalid warnings by readPromptActivationCandidates.
|
|
223
|
+
const promptBudgetHostKind = options.hostKind === 'openclaw' || options.hostKind === 'codex' || options.hostKind === 'workbuddy'
|
|
224
|
+
? options.hostKind
|
|
225
|
+
: undefined;
|
|
226
|
+
const declaredWindow = promptBudgetHostKind === undefined
|
|
227
|
+
? undefined
|
|
228
|
+
: operatorDeclaredContextWindow(loadPdConfigForPlugin(event.context.workspaceDir).effective.config.promptBudget, promptBudgetHostKind);
|
|
215
229
|
const prompt = await buildActivePrinciplePromptContext({
|
|
216
230
|
workspaceDir: event.context.workspaceDir,
|
|
217
231
|
excludePrincipleIds: options.promptExcludePrincipleIds?.(event),
|
|
@@ -221,6 +235,8 @@ export function createProductionHostRuntime(options = {}) {
|
|
|
221
235
|
// Codex+WorkBuddy model id) against the installed local Pi AI
|
|
222
236
|
// catalog — no network, no LLM call. Missing/untrusted facts
|
|
223
237
|
// resolve to the legacy fixed budget with an observable reason.
|
|
238
|
+
// PRI-1020: an Owner declaration outranks all of these (tier 0).
|
|
239
|
+
...(declaredWindow !== undefined ? { operatorDeclared: { contextWindowTokens: declaredWindow } } : {}),
|
|
224
240
|
hostCapacity: event.context.modelContext === undefined
|
|
225
241
|
? undefined
|
|
226
242
|
: {
|
|
@@ -307,3 +323,6 @@ export * from './behavior-history.js';
|
|
|
307
323
|
export * from './codex-rule-context-provider.js';
|
|
308
324
|
export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
|
|
309
325
|
export * from './explicit-owner-statement-prefix.js';
|
|
326
|
+
export { listObservedDesignGateSessions } from './workspace-behavior-evidence.js';
|
|
327
|
+
export { readDesignGateSpecSnapshot, describeCodexRuleContextFact } from './design-gate-facts.js';
|
|
328
|
+
export { createCodexArtificerTrustedFactsPort } from './codex-artificer-trusted-facts.js';
|
|
@@ -43,6 +43,13 @@ async function withinGateDeadline(value, startedAt) {
|
|
|
43
43
|
function isRecord(value) {
|
|
44
44
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
45
45
|
}
|
|
46
|
+
function requiresDesignGateFact(value) {
|
|
47
|
+
if (!isRecord(value) || !Object.hasOwn(value, 'requiredFactKinds'))
|
|
48
|
+
return false;
|
|
49
|
+
return Array.isArray(value.requiredFactKinds)
|
|
50
|
+
&& value.requiredFactKinds.every((kind) => typeof kind === 'string')
|
|
51
|
+
&& value.requiredFactKinds.includes('design_approval');
|
|
52
|
+
}
|
|
46
53
|
function readToolInput(rawPayload) {
|
|
47
54
|
if (!isRecord(rawPayload) || !Object.hasOwn(rawPayload, 'toolInput'))
|
|
48
55
|
return null;
|
|
@@ -375,7 +382,24 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
375
382
|
// sibling rules (v1 or well-fed v2) keep evaluating, and no
|
|
376
383
|
// governance state changes. Intentional behavior change vs the
|
|
377
384
|
// pre-fix "legal unavailable still executed" path.
|
|
378
|
-
|
|
385
|
+
//
|
|
386
|
+
// PRI-1019 exception: a rule whose ONLY declared dependence is
|
|
387
|
+
// the design_approval fact stays eligible on an actionable fact
|
|
388
|
+
// (resolution !== 'unknown') even when history is unavailable.
|
|
389
|
+
// SPEC-RULECODE-CODEX-LOCAL-CLOSURE-001 §6 requires a managed
|
|
390
|
+
// action with an authoritative not_approved to be blocked
|
|
391
|
+
// regardless of history, and the Artificer prompt contract
|
|
392
|
+
// (V2_CONTEXT_INSTRUCTION) requires history-dependent behavior
|
|
393
|
+
// to allow while the independently supplied fact stays
|
|
394
|
+
// enforceable. Without this exception the first managed action
|
|
395
|
+
// of a new session (no prior history) would never evaluate the
|
|
396
|
+
// rule, and an authoritative not_approved could not block.
|
|
397
|
+
const designGateFact = context?.facts.designGate;
|
|
398
|
+
const designGateActionable = designGateFact !== undefined && designGateFact.resolution !== 'unknown'
|
|
399
|
+
? designGateFact
|
|
400
|
+
: undefined;
|
|
401
|
+
if ((context === undefined || context.history.status !== 'available')
|
|
402
|
+
&& !(requiresDesignGateFact(content.ruleHostInputContract) && designGateActionable !== undefined)) {
|
|
379
403
|
// Reference identity against the frozen sentinel distinguishes
|
|
380
404
|
// "the provider faulted" from "the provider legitimately
|
|
381
405
|
// declared history unavailable" (same wire shape, different
|
|
@@ -388,6 +412,18 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
388
412
|
addWarning(warnings, `rule_context_v2_unavailable: ${fault} (activation=${activationId})`, 'enable and wire the host rule context provider, or regenerate the rule without context dependence');
|
|
389
413
|
continue;
|
|
390
414
|
}
|
|
415
|
+
const inputContract = content.ruleHostInputContract;
|
|
416
|
+
if (isRecord(inputContract) && Object.hasOwn(inputContract, 'requiredFactKinds')) {
|
|
417
|
+
if (!Array.isArray(inputContract.requiredFactKinds)
|
|
418
|
+
|| !inputContract.requiredFactKinds.every((kind) => typeof kind === 'string')) {
|
|
419
|
+
addWarning(warnings, `rule_required_fact_contract_invalid: activation=${activationId}`, 'regenerate the rule artifact from validated Scribe execution conditions');
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (requiresDesignGateFact(inputContract) && designGateActionable === undefined) {
|
|
423
|
+
addWarning(warnings, `rule_required_fact_unavailable: design_approval (activation=${activationId})`, 'verify the current Owner design declaration and scope before retrying');
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
391
427
|
}
|
|
392
428
|
const ruleId = typeof content.ruleId === 'string' ? content.ruleId : typeof row.source_rule_id === 'string' ? row.source_rule_id : artifactId;
|
|
393
429
|
const principleId = typeof content.principleId === 'string' ? content.principleId : typeof row.source_principle_id === 'string' ? row.source_principle_id : ruleId;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PROMPT_BUDGET_POLICY_VERSION, type PromptBudgetCapacitySource, type PromptBudgetCapacityStatus, type PromptBudgetFallbackReason, type PromptBudgetResolution } from '@principles/core/runtime-v2';
|
|
1
|
+
import { PROMPT_BUDGET_POLICY_VERSION, type PromptBudgetCapacitySource, type PromptBudgetCapacityStatus, type PromptBudgetFallbackReason, type PromptBudgetResolution, type PromptBudgetConfig } from '@principles/core/runtime-v2';
|
|
2
2
|
import { type PromptInjectionTargetHost } from './prompt-injection-projection.js';
|
|
3
3
|
/**
|
|
4
4
|
* PRI-1017: runtime budget OBSERVATION read model over the EXISTING
|
|
@@ -174,3 +174,12 @@ export declare function selectConsoleRuntimeBudgetFacts(input: {
|
|
|
174
174
|
* rides events or console payloads.
|
|
175
175
|
*/
|
|
176
176
|
export declare function runtimeBudgetObservationToResolution(observation: RuntimePromptBudgetObservation): PromptBudgetResolution;
|
|
177
|
+
/**
|
|
178
|
+
* PRI-1020: the Owner's declared window as a budget resolution, for console
|
|
179
|
+
* surfaces with NO fresh runtime observation. Goes through the same single
|
|
180
|
+
* resolver (tier 0) so an injected forecast and the console fallback can
|
|
181
|
+
* never disagree about what a declaration means. Undefined when the
|
|
182
|
+
* workspace declares nothing — the caller then keeps the historical fixed
|
|
183
|
+
* fallback.
|
|
184
|
+
*/
|
|
185
|
+
export declare function operatorDeclaredBudgetResolution(promptBudget: PromptBudgetConfig | undefined, hostKind: PromptInjectionTargetHost): PromptBudgetResolution | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { PROMPT_BUDGET_POLICY_VERSION, RUNTIME_V2_PRINCIPLE_BUDGET, isValidContextWindowTokens, promptBudgetCharsForContextWindow, } from '@principles/core/runtime-v2';
|
|
3
|
+
import { PROMPT_BUDGET_POLICY_VERSION, RUNTIME_V2_PRINCIPLE_BUDGET, isValidContextWindowTokens, promptBudgetCharsForContextWindow, resolvePromptBudget, operatorDeclaredContextWindow, } from '@principles/core/runtime-v2';
|
|
4
4
|
import { isPromptInjectionTargetHost, } from './prompt-injection-projection.js';
|
|
5
5
|
/**
|
|
6
6
|
* PRI-1017: runtime budget OBSERVATION read model over the EXISTING
|
|
@@ -104,6 +104,7 @@ function parseObservedAt(value) {
|
|
|
104
104
|
return Number.isFinite(parsed) ? parsed : null;
|
|
105
105
|
}
|
|
106
106
|
const BUDGET_CAPACITY_SOURCES = new Set([
|
|
107
|
+
'operator_declared',
|
|
107
108
|
'host_effective_window',
|
|
108
109
|
'pi_catalog_exact',
|
|
109
110
|
'legacy_fallback',
|
|
@@ -177,7 +178,11 @@ function narrowObservation(parsed, workspaceDir, nowMs) {
|
|
|
177
178
|
// formula derives from it; the legacy fallback needs the legacy budget, no
|
|
178
179
|
// window, and a bounded reason. Anything else is not a trustworthy fact.
|
|
179
180
|
if (rawStatus === 'confirmed') {
|
|
180
|
-
|
|
181
|
+
// PRI-1020: operator_declared joins the confirmed set — a declared window
|
|
182
|
+
// is a real window (the Owner's explicit cap), so its observations carry
|
|
183
|
+
// the window and the exact formula budget like the other confirmed
|
|
184
|
+
// sources.
|
|
185
|
+
if (rawSource !== 'operator_declared' && rawSource !== 'host_effective_window' && rawSource !== 'pi_catalog_exact')
|
|
181
186
|
return null;
|
|
182
187
|
if (!isValidContextWindowTokens(rawWindow))
|
|
183
188
|
return null;
|
|
@@ -447,3 +452,17 @@ export function runtimeBudgetObservationToResolution(observation) {
|
|
|
447
452
|
...(observation.fallbackReason !== null ? { fallbackReason: observation.fallbackReason } : {}),
|
|
448
453
|
};
|
|
449
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* PRI-1020: the Owner's declared window as a budget resolution, for console
|
|
457
|
+
* surfaces with NO fresh runtime observation. Goes through the same single
|
|
458
|
+
* resolver (tier 0) so an injected forecast and the console fallback can
|
|
459
|
+
* never disagree about what a declaration means. Undefined when the
|
|
460
|
+
* workspace declares nothing — the caller then keeps the historical fixed
|
|
461
|
+
* fallback.
|
|
462
|
+
*/
|
|
463
|
+
export function operatorDeclaredBudgetResolution(promptBudget, hostKind) {
|
|
464
|
+
const declaredWindow = operatorDeclaredContextWindow(promptBudget, hostKind);
|
|
465
|
+
if (declaredWindow === undefined)
|
|
466
|
+
return undefined;
|
|
467
|
+
return resolvePromptBudget({ operatorDeclared: { contextWindowTokens: declaredWindow } });
|
|
468
|
+
}
|
|
@@ -19,3 +19,12 @@ export declare function readWorkspaceRuleContextRowsBefore(workspaceDir: string,
|
|
|
19
19
|
truncated: boolean;
|
|
20
20
|
unscopedPriorRows: boolean;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* List only Codex sessions proven by normalized host evidence and a matching
|
|
24
|
+
* project-scoped trajectory row. The trajectory schema has no host_kind on
|
|
25
|
+
* tool_calls, so that table alone is never used to infer the host.
|
|
26
|
+
*/
|
|
27
|
+
export declare function listObservedDesignGateSessions(workspaceDir: string): {
|
|
28
|
+
projectWorkspacePath: string;
|
|
29
|
+
hostSessionId: string;
|
|
30
|
+
}[];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Read-only adapter over the canonical trajectory store; never creates/migrates it. */
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import Database from 'better-sqlite3';
|
|
5
|
+
import { SqliteConnection } from '@principles/core/runtime-v2';
|
|
4
6
|
import { createOwnerEvidenceScopeError } from '@principles/core/runtime-v2/internalization/evidence-readiness';
|
|
5
7
|
import { BehaviorExamplePackAssembler } from './behavior-example-pack-assembler.js';
|
|
6
8
|
import { resolveWorkspaceHostToolSemantics } from './host-tool-semantic-resolver.js';
|
|
@@ -119,3 +121,86 @@ export function readWorkspaceRuleContextRowsBefore(workspaceDir, input) {
|
|
|
119
121
|
db.close();
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* List only Codex sessions proven by normalized host evidence and a matching
|
|
126
|
+
* project-scoped trajectory row. The trajectory schema has no host_kind on
|
|
127
|
+
* tool_calls, so that table alone is never used to infer the host.
|
|
128
|
+
*/
|
|
129
|
+
export function listObservedDesignGateSessions(workspaceDir) {
|
|
130
|
+
const connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
131
|
+
let evidenceRefs;
|
|
132
|
+
try {
|
|
133
|
+
const db = connection.getDb();
|
|
134
|
+
const exists = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'intervention_evidence_records'").get();
|
|
135
|
+
if (exists === undefined)
|
|
136
|
+
return [];
|
|
137
|
+
const rawRows = db.prepare(`
|
|
138
|
+
SELECT native_refs_json, recorded_at
|
|
139
|
+
FROM intervention_evidence_records
|
|
140
|
+
ORDER BY recorded_at DESC
|
|
141
|
+
LIMIT 1000
|
|
142
|
+
`).all();
|
|
143
|
+
const found = new Map();
|
|
144
|
+
for (const raw of rawRows) {
|
|
145
|
+
if (!record(raw) || typeof raw.native_refs_json !== 'string' || typeof raw.recorded_at !== 'string')
|
|
146
|
+
continue;
|
|
147
|
+
let refs;
|
|
148
|
+
try {
|
|
149
|
+
refs = JSON.parse(raw.native_refs_json);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!record(refs) || refs.hostKind !== 'codex' || typeof refs.sessionId !== 'string' || refs.sessionId.trim().length === 0)
|
|
155
|
+
continue;
|
|
156
|
+
if (!found.has(refs.sessionId))
|
|
157
|
+
found.set(refs.sessionId, raw.recorded_at);
|
|
158
|
+
}
|
|
159
|
+
evidenceRefs = [...found].map(([hostSessionId, lastObservedAt]) => ({ hostSessionId, lastObservedAt }));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
connection.close();
|
|
163
|
+
}
|
|
164
|
+
const trajectoryPath = path.join(workspaceDir, '.state', 'trajectory.db');
|
|
165
|
+
const trajectory = new Database(trajectoryPath, { readonly: true, fileMustExist: true });
|
|
166
|
+
try {
|
|
167
|
+
const selectProject = trajectory.prepare(`
|
|
168
|
+
SELECT project_dir, MAX(created_at) AS last_seen
|
|
169
|
+
FROM tool_calls
|
|
170
|
+
WHERE session_id = ? AND project_dir IS NOT NULL
|
|
171
|
+
GROUP BY project_dir
|
|
172
|
+
ORDER BY last_seen DESC
|
|
173
|
+
`);
|
|
174
|
+
const candidates = [];
|
|
175
|
+
for (const evidence of evidenceRefs) {
|
|
176
|
+
const rows = selectProject.all(evidence.hostSessionId);
|
|
177
|
+
for (const raw of rows) {
|
|
178
|
+
if (!record(raw) || typeof raw.project_dir !== 'string' || !path.isAbsolute(raw.project_dir)
|
|
179
|
+
|| typeof raw.last_seen !== 'string')
|
|
180
|
+
continue;
|
|
181
|
+
try {
|
|
182
|
+
candidates.push({
|
|
183
|
+
projectWorkspacePath: fs.realpathSync.native(raw.project_dir),
|
|
184
|
+
hostSessionId: evidence.hostSessionId,
|
|
185
|
+
seenAt: raw.last_seen > evidence.lastObservedAt ? raw.last_seen : evidence.lastObservedAt,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch { /* stale project paths are not offered as selectable sessions */ }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const unique = new Map();
|
|
192
|
+
for (const candidate of candidates) {
|
|
193
|
+
const key = `${candidate.projectWorkspacePath}\0${candidate.hostSessionId}`;
|
|
194
|
+
const current = unique.get(key);
|
|
195
|
+
if (!current || candidate.seenAt > current.seenAt)
|
|
196
|
+
unique.set(key, candidate);
|
|
197
|
+
}
|
|
198
|
+
return [...unique.values()]
|
|
199
|
+
.sort((left, right) => right.seenAt.localeCompare(left.seenAt))
|
|
200
|
+
.slice(0, 100)
|
|
201
|
+
.map(({ projectWorkspacePath, hostSessionId }) => ({ projectWorkspacePath, hostSessionId }));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
trajectory.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint": "eslint \"src/**/*.ts\""
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@principles/core": "^1.
|
|
34
|
+
"@principles/core": "^1.296.0",
|
|
35
35
|
"@principles/install-layout": "^0.4.0",
|
|
36
36
|
"better-sqlite3": "^13.0.3",
|
|
37
37
|
"js-yaml": "^5.4.3"
|