@principles/host-runtime 0.12.1 → 0.12.2
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 +3 -0
- package/dist/production-rulehost-gate.js +37 -1
- 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
|
@@ -307,3 +307,6 @@ export * from './behavior-history.js';
|
|
|
307
307
|
export * from './codex-rule-context-provider.js';
|
|
308
308
|
export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
|
|
309
309
|
export * from './explicit-owner-statement-prefix.js';
|
|
310
|
+
export { listObservedDesignGateSessions } from './workspace-behavior-evidence.js';
|
|
311
|
+
export { readDesignGateSpecSnapshot, describeCodexRuleContextFact } from './design-gate-facts.js';
|
|
312
|
+
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;
|
|
@@ -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.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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.295.
|
|
34
|
+
"@principles/core": "^1.295.3",
|
|
35
35
|
"@principles/install-layout": "^0.4.0",
|
|
36
36
|
"better-sqlite3": "^13.0.3",
|
|
37
37
|
"js-yaml": "^5.4.3"
|