@guilz-dev/belay 0.7.0 → 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/README.md +19 -19
- package/dist/adapters/shared/gate-runtime.js +23 -5
- package/dist/bundle/claude-runtime.mjs +4909 -3504
- package/dist/bundle/codex-runtime.mjs +5050 -3645
- package/dist/bundle/cursor-runtime.mjs +5050 -3645
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/dogfood.js +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/harvest.d.ts +0 -1
- package/dist/commands/harvest.js +1 -4
- package/dist/commands/metrics.js +6 -0
- package/dist/commands/quality.js +1 -3
- package/dist/conformance/guarantee-table.js +133 -11
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/audit-metrics.d.ts +1 -0
- package/dist/core/audit-metrics.js +12 -4
- package/dist/core/audit-types.d.ts +3 -0
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/policy-engine.d.ts +31 -1
- package/dist/core/capability/policy-engine.js +186 -0
- package/dist/core/capability/request.d.ts +9 -0
- package/dist/core/capability/resolver.d.ts +0 -1
- package/dist/core/capability/resolver.js +0 -1
- package/dist/core/config.js +0 -2
- package/dist/core/effect-ir/audit.js +1 -0
- package/dist/core/effect-ir/build.js +5 -1
- package/dist/core/effect-ir/index.d.ts +6 -1
- package/dist/core/effect-ir/index.js +2 -0
- package/dist/core/effect-ir/normalize.js +9 -3
- package/dist/core/effect-ir/policy.d.ts +2 -1
- package/dist/core/effect-ir/policy.js +93 -6
- package/dist/core/effect-ir/shell-build.d.ts +43 -0
- package/dist/core/effect-ir/shell-build.js +77 -0
- package/dist/core/effect-ir/shell-lower.d.ts +14 -0
- package/dist/core/effect-ir/shell-lower.js +1509 -0
- package/dist/core/effect-ir/types.d.ts +9 -2
- package/dist/core/gate-engine.js +63 -23
- package/dist/core/git-resource-identity.d.ts +26 -0
- package/dist/core/git-resource-identity.js +284 -0
- package/dist/core/harvest.d.ts +3 -7
- package/dist/core/harvest.js +4 -27
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/path-utils.js +42 -23
- package/dist/core/shell-substitution.d.ts +1 -0
- package/dist/core/shell-substitution.js +61 -0
- package/dist/core/standing-allow.d.ts +1 -2
- package/dist/core/standing-allow.js +3 -20
- package/dist/core/types.d.ts +4 -1
- package/dist/core/verdict/adapter.js +15 -15
- package/dist/core/verdict/containment.js +4 -0
- package/dist/core/verdict/egress-classify.d.ts +12 -0
- package/dist/core/verdict/egress-classify.js +746 -0
- package/dist/core/verdict/git-classifier.d.ts +12 -0
- package/dist/core/verdict/git-classifier.js +320 -0
- package/dist/core/verdict/launcher-resolve.js +77 -17
- package/dist/core/verdict/parser.d.ts +2 -0
- package/dist/core/verdict/parser.js +95 -1
- package/dist/core/verdict/types.d.ts +2 -3
- package/dist/core/verdict/verdict.js +103 -952
- package/dist/corpus/must-allow-commands.d.ts +2 -1
- package/dist/corpus/must-allow-commands.js +2 -1
- package/dist/corpus/runtime-match.d.ts +2 -1
- package/dist/corpus/runtime-match.js +2 -1
- package/dist/corpus/types.d.ts +2 -2
- package/dist/templates.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/core/verdict/overrides.d.ts +0 -7
- package/dist/core/verdict/overrides.js +0 -37
- package/dist/core/verdict/shell-policy.d.ts +0 -26
- package/dist/core/verdict/shell-policy.js +0 -40
- package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
- package/dist/corpus/standing-allow-catalog.generated.js +0 -99
|
@@ -4,6 +4,13 @@ export declare const EFFECT_PLAN_VERSION: 1;
|
|
|
4
4
|
export type EffectTag = 'fs.read' | 'fs.write' | 'process.exec' | 'network.connect' | 'network.acquire' | 'git.ref.write' | 'secret.read' | 'control_plane.write' | 'read_only' | 'indeterminate';
|
|
5
5
|
export type LauncherPhase = 'invoke' | 'resolve' | 'acquire' | 'cache_read' | 'cache_write' | 'delegate';
|
|
6
6
|
export type PackageExecLauncher = 'npx' | 'npm' | 'pnpm';
|
|
7
|
+
export type EffectPlanDisposition = 'effects' | 'effect_free';
|
|
8
|
+
export type AnalysisCompleteness = 'complete' | 'partial';
|
|
9
|
+
export interface EffectPlanPolicyProjection {
|
|
10
|
+
permission: 'allow' | 'ask';
|
|
11
|
+
hookVerdict: 'allow' | 'allow_flagged' | 'deny_pending_approval';
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
7
14
|
export interface EffectEvidence {
|
|
8
15
|
level: CapabilityEvidenceLevel;
|
|
9
16
|
signals: readonly string[];
|
|
@@ -49,7 +56,7 @@ export interface EffectPlan {
|
|
|
49
56
|
root: EffectNode;
|
|
50
57
|
inputFingerprint: string;
|
|
51
58
|
opacity: VerdictOpacity;
|
|
52
|
-
disposition:
|
|
53
|
-
completeness:
|
|
59
|
+
disposition: EffectPlanDisposition;
|
|
60
|
+
completeness: AnalysisCompleteness;
|
|
54
61
|
signals: readonly string[];
|
|
55
62
|
}
|
package/dist/core/gate-engine.js
CHANGED
|
@@ -5,9 +5,11 @@ import { classifySubagent } from './classify-subagent.js';
|
|
|
5
5
|
import { classifyToolUse } from './classify-tool.js';
|
|
6
6
|
import { classifierOptionsFromConfig } from './config.js';
|
|
7
7
|
import { buildCapabilityEffectPlan } from './effect-ir/build.js';
|
|
8
|
+
import { evaluateEffectPlanPolicy } from './effect-ir/policy.js';
|
|
9
|
+
import { buildShellEffectPlan } from './effect-ir/shell-build.js';
|
|
8
10
|
import { GATE_CONTRACT_VERSION } from './gate-contract.js';
|
|
9
11
|
import { mergeAgentAssessment } from './judgment.js';
|
|
10
|
-
import { classifyShell } from './verdict/adapter.js';
|
|
12
|
+
import { buildVerdictContext, classifyShell } from './verdict/adapter.js';
|
|
11
13
|
export class GateNormalizationError extends Error {
|
|
12
14
|
reason = 'normalization_failed';
|
|
13
15
|
constructor(message) {
|
|
@@ -168,16 +170,6 @@ function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
|
|
|
168
170
|
}
|
|
169
171
|
return result;
|
|
170
172
|
}
|
|
171
|
-
function applySandboxOutsideBoundary(command, action, result, options) {
|
|
172
|
-
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
|
|
173
|
-
return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
|
|
174
|
-
redirect: command.includes('>'),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
function applyShellPeripheralPolicy(command, action, result, options) {
|
|
178
|
-
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
|
|
179
|
-
return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
|
|
180
|
-
}
|
|
181
173
|
function outsideRepoPathsForToolAction(action, options) {
|
|
182
174
|
const payload = action.payload ?? {};
|
|
183
175
|
const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot, options.trustedWorkspaceRoots));
|
|
@@ -199,32 +191,28 @@ function applyToolSandboxPolicies(action, result, options) {
|
|
|
199
191
|
return next;
|
|
200
192
|
}
|
|
201
193
|
export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
194
|
+
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
202
195
|
const limitResult = checkGatedActionLimits(action);
|
|
203
196
|
if (limitResult) {
|
|
197
|
+
if (action.kind === 'shell') {
|
|
198
|
+
return projectShellLimitResult(action, limitResult, config, options);
|
|
199
|
+
}
|
|
204
200
|
return attachEffectPlan(action, limitResult);
|
|
205
201
|
}
|
|
206
|
-
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
207
202
|
if (action.kind === 'shell') {
|
|
208
203
|
const command = action.command ?? shellCommandFromPayload(action.payload ?? {});
|
|
209
204
|
if (!command) {
|
|
210
205
|
throw new GateNormalizationError('Shell gated action requires a command.');
|
|
211
206
|
}
|
|
212
|
-
|
|
213
|
-
result = applySandboxOutsideBoundary(command, action, result, options);
|
|
214
|
-
result = applyShellPeripheralPolicy(command, action, result, options);
|
|
207
|
+
const result = await classifyShell(command, action.cwd, action.repoRoot, config, options);
|
|
215
208
|
if (!action.agentAssessment) {
|
|
216
|
-
return
|
|
209
|
+
return result;
|
|
217
210
|
}
|
|
218
211
|
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
219
|
-
|
|
220
|
-
return attachEffectPlan(action, { ...result, assessment: merged.assessment });
|
|
221
|
-
}
|
|
222
|
-
return attachEffectPlan(action, {
|
|
212
|
+
return {
|
|
223
213
|
...result,
|
|
224
|
-
verdict: 'deny_pending_approval',
|
|
225
|
-
reason: 'agent_assessment_mismatch',
|
|
226
214
|
assessment: merged.assessment,
|
|
227
|
-
}
|
|
215
|
+
};
|
|
228
216
|
}
|
|
229
217
|
if (action.kind === 'subagent') {
|
|
230
218
|
return attachEffectPlan(action, classifySubagent(action.payload ?? {}, action.repoRoot, options, config));
|
|
@@ -245,6 +233,58 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
245
233
|
assessment: merged.assessment,
|
|
246
234
|
});
|
|
247
235
|
}
|
|
236
|
+
function projectShellLimitResult(action, result, config, options) {
|
|
237
|
+
const summary = result.summary ?? 'oversized shell input';
|
|
238
|
+
const effectPlan = buildShellEffectPlan({
|
|
239
|
+
inputFingerprint: result.fingerprint,
|
|
240
|
+
segments: [
|
|
241
|
+
{
|
|
242
|
+
commandRedacted: summary,
|
|
243
|
+
segmentHead: 'input-limit',
|
|
244
|
+
requirements: [
|
|
245
|
+
{
|
|
246
|
+
tag: 'indeterminate',
|
|
247
|
+
action: 'indeterminate',
|
|
248
|
+
resource: { kind: 'unknown' },
|
|
249
|
+
evidence: {
|
|
250
|
+
level: 'indeterminate',
|
|
251
|
+
signals: ['input_too_large', 'analysis_budget_exceeded'],
|
|
252
|
+
basis: ['gate:input_limit'],
|
|
253
|
+
},
|
|
254
|
+
provenance: { segment: summary },
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
completeness: 'partial',
|
|
258
|
+
opacity: 'opaque',
|
|
259
|
+
signals: ['input_too_large', 'analysis_budget_exceeded'],
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
const policy = evaluateEffectPlanPolicy(effectPlan, buildVerdictContext({
|
|
264
|
+
cwd: action.cwd,
|
|
265
|
+
repoRoot: action.repoRoot,
|
|
266
|
+
config,
|
|
267
|
+
options,
|
|
268
|
+
}));
|
|
269
|
+
return {
|
|
270
|
+
...result,
|
|
271
|
+
verdict: policy.projection.hookVerdict,
|
|
272
|
+
axes: {
|
|
273
|
+
location: 'unknown',
|
|
274
|
+
opacity: effectPlan.opacity,
|
|
275
|
+
effect: 'unknown',
|
|
276
|
+
confidence: 'deterministic',
|
|
277
|
+
would: policy.projection.permission,
|
|
278
|
+
by: 'effect_plan',
|
|
279
|
+
signals: [...effectPlan.signals],
|
|
280
|
+
},
|
|
281
|
+
capabilityRequests: policy.capabilityRequests,
|
|
282
|
+
authorizationDecision: policy.authorizationDecision,
|
|
283
|
+
effectPlan,
|
|
284
|
+
effectPlanPolicyDecisions: policy.decisions,
|
|
285
|
+
effectPlanProjection: policy.projection,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
248
288
|
function attachEffectPlan(action, result) {
|
|
249
289
|
if (result.effectPlan) {
|
|
250
290
|
return result;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface GitResourceIdentity {
|
|
2
|
+
repositoryRoot: string;
|
|
3
|
+
gitDir: string;
|
|
4
|
+
commonDir: string;
|
|
5
|
+
gitEntryPath: string;
|
|
6
|
+
}
|
|
7
|
+
export type GitResourceIdentityInspection = {
|
|
8
|
+
status: 'resolved';
|
|
9
|
+
identity: GitResourceIdentity;
|
|
10
|
+
} | {
|
|
11
|
+
status: 'absent';
|
|
12
|
+
} | {
|
|
13
|
+
status: 'invalid';
|
|
14
|
+
boundaryPath: string;
|
|
15
|
+
metadataRoots: string[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Inspects the Git boundary containing a path without executing Git or a shell.
|
|
19
|
+
* Malformed or unreadable metadata is distinct from an absent Git boundary.
|
|
20
|
+
*/
|
|
21
|
+
export declare function inspectGitResourceIdentity(targetPath: string): GitResourceIdentityInspection;
|
|
22
|
+
/** Resolves only structurally valid Git metadata; otherwise returns null. */
|
|
23
|
+
export declare function resolveGitResourceIdentity(targetPath: string): GitResourceIdentity | null;
|
|
24
|
+
export declare function sameGitResourceIdentity(leftPath: string, rightPath: string): boolean;
|
|
25
|
+
/** Protects control data for any proven repository and any malformed Git boundary. */
|
|
26
|
+
export declare function isGitMetadataPath(targetPath: string, _repositoryPath?: string): boolean;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function canonicalExistingPath(targetPath) {
|
|
4
|
+
try {
|
|
5
|
+
return realpathSync.native(targetPath);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function isDirectory(targetPath) {
|
|
12
|
+
try {
|
|
13
|
+
return statSync(targetPath).isDirectory();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function nearestExistingDirectory(targetPath) {
|
|
20
|
+
let current = path.resolve(targetPath);
|
|
21
|
+
while (true) {
|
|
22
|
+
try {
|
|
23
|
+
const stats = statSync(current);
|
|
24
|
+
const directory = stats.isDirectory() ? current : path.dirname(current);
|
|
25
|
+
const canonical = canonicalExistingPath(directory);
|
|
26
|
+
return canonical ? { status: 'resolved', directory: canonical } : { status: 'invalid' };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const code = error.code;
|
|
30
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
31
|
+
return { status: 'invalid' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const parent = path.dirname(current);
|
|
35
|
+
if (parent === current) {
|
|
36
|
+
return { status: 'invalid' };
|
|
37
|
+
}
|
|
38
|
+
current = parent;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function readSingleMetadataLine(metadataPath) {
|
|
42
|
+
try {
|
|
43
|
+
const value = readFileSync(metadataPath, 'utf8').trim();
|
|
44
|
+
if (!value || value.includes('\0') || value.includes('\n') || value.includes('\r')) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function pathEntryStatus(targetPath) {
|
|
54
|
+
try {
|
|
55
|
+
lstatSync(targetPath);
|
|
56
|
+
return 'present';
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code === 'ENOENT') {
|
|
60
|
+
return 'absent';
|
|
61
|
+
}
|
|
62
|
+
return 'unreadable';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function resolveMetadataDirectory(value, baseDirectory) {
|
|
66
|
+
const candidate = path.isAbsolute(value) ? value : path.resolve(baseDirectory, value);
|
|
67
|
+
if (!isDirectory(candidate)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return canonicalExistingPath(candidate);
|
|
71
|
+
}
|
|
72
|
+
function hasValidCommonDirStructure(commonDir) {
|
|
73
|
+
return (hasValidHead(commonDir) &&
|
|
74
|
+
hasValidGitConfig(commonDir) &&
|
|
75
|
+
isDirectory(path.join(commonDir, 'objects')) &&
|
|
76
|
+
isDirectory(path.join(commonDir, 'refs')));
|
|
77
|
+
}
|
|
78
|
+
function hasValidHead(gitDir) {
|
|
79
|
+
const head = readSingleMetadataLine(path.join(gitDir, 'HEAD'));
|
|
80
|
+
return head !== null && (/^ref:\s+refs\/\S+$/.test(head) || /^[0-9a-f]{40,64}$/i.test(head));
|
|
81
|
+
}
|
|
82
|
+
function hasValidGitConfig(gitDir) {
|
|
83
|
+
const configPath = path.join(gitDir, 'config');
|
|
84
|
+
try {
|
|
85
|
+
return (statSync(configPath).isFile() && /^\s*\[core\]\s*$/im.test(readFileSync(configPath, 'utf8')));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function looksLikeBareBoundary(candidate) {
|
|
92
|
+
const markers = [
|
|
93
|
+
pathEntryStatus(path.join(candidate, 'HEAD')),
|
|
94
|
+
pathEntryStatus(path.join(candidate, 'config')),
|
|
95
|
+
pathEntryStatus(path.join(candidate, 'objects')),
|
|
96
|
+
pathEntryStatus(path.join(candidate, 'refs')),
|
|
97
|
+
];
|
|
98
|
+
return markers.filter((status) => status !== 'absent').length >= 2;
|
|
99
|
+
}
|
|
100
|
+
function invalidInspection(boundaryPath, metadataRoots) {
|
|
101
|
+
return {
|
|
102
|
+
status: 'invalid',
|
|
103
|
+
boundaryPath,
|
|
104
|
+
metadataRoots: [...new Set(metadataRoots.map((root) => path.resolve(root)))],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function resolveBareIdentity(repositoryRoot) {
|
|
108
|
+
if (!looksLikeBareBoundary(repositoryRoot)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (!hasValidCommonDirStructure(repositoryRoot)) {
|
|
112
|
+
return invalidInspection(repositoryRoot, [repositoryRoot]);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
status: 'resolved',
|
|
116
|
+
identity: {
|
|
117
|
+
repositoryRoot,
|
|
118
|
+
gitDir: repositoryRoot,
|
|
119
|
+
commonDir: repositoryRoot,
|
|
120
|
+
gitEntryPath: repositoryRoot,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function resolveLinkedIdentity(repositoryRoot, gitEntryPath) {
|
|
125
|
+
const forwardMetadata = readSingleMetadataLine(gitEntryPath);
|
|
126
|
+
const match = forwardMetadata?.match(/^gitdir:\s*(.+)$/);
|
|
127
|
+
const gitDirValue = match?.[1]?.trim();
|
|
128
|
+
if (!gitDirValue) {
|
|
129
|
+
return invalidInspection(repositoryRoot, [gitEntryPath]);
|
|
130
|
+
}
|
|
131
|
+
const gitDir = resolveMetadataDirectory(gitDirValue, repositoryRoot);
|
|
132
|
+
if (!gitDir) {
|
|
133
|
+
return invalidInspection(repositoryRoot, [gitEntryPath]);
|
|
134
|
+
}
|
|
135
|
+
const commonDirValue = readSingleMetadataLine(path.join(gitDir, 'commondir'));
|
|
136
|
+
const commonDir = commonDirValue ? resolveMetadataDirectory(commonDirValue, gitDir) : null;
|
|
137
|
+
const backpointerValue = readSingleMetadataLine(path.join(gitDir, 'gitdir'));
|
|
138
|
+
const backpointer = backpointerValue
|
|
139
|
+
? canonicalExistingPath(path.isAbsolute(backpointerValue)
|
|
140
|
+
? backpointerValue
|
|
141
|
+
: path.resolve(gitDir, backpointerValue))
|
|
142
|
+
: null;
|
|
143
|
+
const canonicalGitEntry = canonicalExistingPath(gitEntryPath);
|
|
144
|
+
const expectedWorktreesRoot = commonDir ? path.join(commonDir, 'worktrees') : null;
|
|
145
|
+
const linkedStructureValid = hasValidHead(gitDir) &&
|
|
146
|
+
commonDir !== null &&
|
|
147
|
+
hasValidCommonDirStructure(commonDir) &&
|
|
148
|
+
expectedWorktreesRoot !== null &&
|
|
149
|
+
path.dirname(gitDir) === expectedWorktreesRoot &&
|
|
150
|
+
backpointer !== null &&
|
|
151
|
+
canonicalGitEntry !== null &&
|
|
152
|
+
backpointer === canonicalGitEntry;
|
|
153
|
+
if (!linkedStructureValid || !commonDir || !canonicalGitEntry) {
|
|
154
|
+
return invalidInspection(repositoryRoot, [
|
|
155
|
+
gitEntryPath,
|
|
156
|
+
gitDir,
|
|
157
|
+
...(commonDir ? [commonDir] : []),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
status: 'resolved',
|
|
162
|
+
identity: {
|
|
163
|
+
repositoryRoot,
|
|
164
|
+
gitDir,
|
|
165
|
+
commonDir,
|
|
166
|
+
gitEntryPath: canonicalGitEntry,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function inspectWorktreeBoundary(repositoryRoot) {
|
|
171
|
+
const gitEntryPath = path.join(repositoryRoot, '.git');
|
|
172
|
+
const entryStatus = pathEntryStatus(gitEntryPath);
|
|
173
|
+
if (entryStatus === 'absent') {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (entryStatus === 'unreadable') {
|
|
177
|
+
return invalidInspection(repositoryRoot, [gitEntryPath]);
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
if (lstatSync(gitEntryPath).isSymbolicLink()) {
|
|
181
|
+
return invalidInspection(repositoryRoot, [gitEntryPath]);
|
|
182
|
+
}
|
|
183
|
+
const entryStats = statSync(gitEntryPath);
|
|
184
|
+
if (entryStats.isFile()) {
|
|
185
|
+
return resolveLinkedIdentity(repositoryRoot, gitEntryPath);
|
|
186
|
+
}
|
|
187
|
+
if (!entryStats.isDirectory()) {
|
|
188
|
+
return invalidInspection(repositoryRoot, [gitEntryPath]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return invalidInspection(repositoryRoot, [gitEntryPath]);
|
|
193
|
+
}
|
|
194
|
+
const gitDir = canonicalExistingPath(gitEntryPath);
|
|
195
|
+
const canonicalGitEntry = canonicalExistingPath(gitEntryPath);
|
|
196
|
+
if (!gitDir || !canonicalGitEntry || !hasValidCommonDirStructure(gitDir)) {
|
|
197
|
+
return invalidInspection(repositoryRoot, [gitEntryPath]);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
status: 'resolved',
|
|
201
|
+
identity: {
|
|
202
|
+
repositoryRoot,
|
|
203
|
+
gitDir,
|
|
204
|
+
commonDir: gitDir,
|
|
205
|
+
gitEntryPath: canonicalGitEntry,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Inspects the Git boundary containing a path without executing Git or a shell.
|
|
211
|
+
* Malformed or unreadable metadata is distinct from an absent Git boundary.
|
|
212
|
+
*/
|
|
213
|
+
export function inspectGitResourceIdentity(targetPath) {
|
|
214
|
+
const nearest = nearestExistingDirectory(targetPath);
|
|
215
|
+
if (nearest.status === 'invalid') {
|
|
216
|
+
return invalidInspection(path.resolve(targetPath), [path.resolve(targetPath)]);
|
|
217
|
+
}
|
|
218
|
+
let current = nearest.directory;
|
|
219
|
+
while (true) {
|
|
220
|
+
const worktree = inspectWorktreeBoundary(current);
|
|
221
|
+
if (worktree) {
|
|
222
|
+
return worktree;
|
|
223
|
+
}
|
|
224
|
+
const bare = resolveBareIdentity(current);
|
|
225
|
+
if (bare) {
|
|
226
|
+
return bare;
|
|
227
|
+
}
|
|
228
|
+
const parent = path.dirname(current);
|
|
229
|
+
if (parent === current) {
|
|
230
|
+
return { status: 'absent' };
|
|
231
|
+
}
|
|
232
|
+
current = parent;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Resolves only structurally valid Git metadata; otherwise returns null. */
|
|
236
|
+
export function resolveGitResourceIdentity(targetPath) {
|
|
237
|
+
const inspection = inspectGitResourceIdentity(targetPath);
|
|
238
|
+
return inspection.status === 'resolved' ? inspection.identity : null;
|
|
239
|
+
}
|
|
240
|
+
export function sameGitResourceIdentity(leftPath, rightPath) {
|
|
241
|
+
const left = resolveGitResourceIdentity(leftPath);
|
|
242
|
+
const right = resolveGitResourceIdentity(rightPath);
|
|
243
|
+
return left !== null && right !== null && left.commonDir === right.commonDir;
|
|
244
|
+
}
|
|
245
|
+
function canonicalTargetPath(targetPath) {
|
|
246
|
+
const resolved = path.resolve(targetPath);
|
|
247
|
+
let current = resolved;
|
|
248
|
+
const suffix = [];
|
|
249
|
+
while (true) {
|
|
250
|
+
const canonical = canonicalExistingPath(current);
|
|
251
|
+
if (canonical) {
|
|
252
|
+
return path.join(canonical, ...suffix);
|
|
253
|
+
}
|
|
254
|
+
const parent = path.dirname(current);
|
|
255
|
+
if (parent === current) {
|
|
256
|
+
return resolved;
|
|
257
|
+
}
|
|
258
|
+
suffix.unshift(path.basename(current));
|
|
259
|
+
current = parent;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function pathWithin(root, targetPath) {
|
|
263
|
+
const relative = path.relative(root, targetPath);
|
|
264
|
+
return (relative === '' ||
|
|
265
|
+
(!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`)));
|
|
266
|
+
}
|
|
267
|
+
/** Protects control data for any proven repository and any malformed Git boundary. */
|
|
268
|
+
export function isGitMetadataPath(targetPath, _repositoryPath) {
|
|
269
|
+
const inspection = inspectGitResourceIdentity(targetPath);
|
|
270
|
+
const target = canonicalTargetPath(targetPath);
|
|
271
|
+
if (inspection.status === 'invalid') {
|
|
272
|
+
return inspection.metadataRoots.some((root) => {
|
|
273
|
+
const canonicalRoot = canonicalTargetPath(root);
|
|
274
|
+
return target === canonicalRoot || pathWithin(canonicalRoot, target);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (inspection.status !== 'resolved') {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const { identity } = inspection;
|
|
281
|
+
return (target === identity.gitEntryPath ||
|
|
282
|
+
pathWithin(identity.gitDir, target) ||
|
|
283
|
+
pathWithin(identity.commonDir, target));
|
|
284
|
+
}
|
package/dist/core/harvest.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CorpusCase } from '../corpus/types.js';
|
|
2
2
|
import type { AuditRecord } from './audit-types.js';
|
|
3
3
|
export declare const HARVEST_REPORT_SCHEMA_VERSION = 1;
|
|
4
|
-
export type HarvestCandidateSource = 'deny_then_approve' | 'repeated_ask' | 'read_style_signal'
|
|
4
|
+
export type HarvestCandidateSource = 'deny_then_approve' | 'repeated_ask' | 'read_style_signal';
|
|
5
5
|
export type HarvestReviewOutcome = 'provably-benign' | 'accepted-benign' | 'reject';
|
|
6
6
|
export interface HarvestCandidate {
|
|
7
7
|
kind: 'shell';
|
|
@@ -35,12 +35,8 @@ export declare function filterRecordsForHarvest(records: AuditRecord[], options?
|
|
|
35
35
|
since?: string;
|
|
36
36
|
until?: string;
|
|
37
37
|
}): AuditRecord[];
|
|
38
|
-
export declare function extractHarvestCandidates(records: AuditRecord[]
|
|
39
|
-
|
|
40
|
-
}): HarvestCandidate[];
|
|
41
|
-
export declare function buildHarvestReport(records: AuditRecord[], options?: {
|
|
42
|
-
allowPatterns?: string[];
|
|
43
|
-
}): HarvestReport;
|
|
38
|
+
export declare function extractHarvestCandidates(records: AuditRecord[]): HarvestCandidate[];
|
|
39
|
+
export declare function buildHarvestReport(records: AuditRecord[]): HarvestReport;
|
|
44
40
|
export declare function applyHarvestReview(cases: CorpusCase[], params: {
|
|
45
41
|
command: string;
|
|
46
42
|
outcome: HarvestReviewOutcome;
|
package/dist/core/harvest.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { computeRepeatedFingerprintAsks, isAvailabilityCausedAsk } from './audit-analysis.js';
|
|
2
2
|
import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, isShellGateRecord, parseTimestamp, } from './audit-query.js';
|
|
3
|
-
import { matchesCustomCommand } from './custom-command-match.js';
|
|
4
3
|
export const HARVEST_REPORT_SCHEMA_VERSION = 1;
|
|
5
4
|
const READ_STYLE_COMMAND_PATTERN = /^\s*(git\s+status|git\s+diff|git\s+log|git\s+show|ls\b|cat\b|head\b|tail\b|find\b|rg\b|grep\b|fd\b|bat\b|less\b|more\b|pwd\b|whoami\b|env\b|printenv\b|npm\s+(test|run\s+test)|pnpm\s+(test|run\s+test)|yarn\s+test)/;
|
|
6
5
|
function shellRecords(records) {
|
|
@@ -145,7 +144,7 @@ export function filterRecordsForHarvest(records, options = {}) {
|
|
|
145
144
|
});
|
|
146
145
|
return augmentHarvestRoundTripPairs(scoped, timeFiltered);
|
|
147
146
|
}
|
|
148
|
-
export function extractHarvestCandidates(records
|
|
147
|
+
export function extractHarvestCandidates(records) {
|
|
149
148
|
const shellOnly = shellRecords(records);
|
|
150
149
|
const classifierAsks = shellOnly.filter((record) => inferWouldBlock(record) && !isAvailabilityCausedAsk(record));
|
|
151
150
|
const map = new Map();
|
|
@@ -187,28 +186,6 @@ export function extractHarvestCandidates(records, options = {}) {
|
|
|
187
186
|
source: 'read_style_signal',
|
|
188
187
|
});
|
|
189
188
|
}
|
|
190
|
-
// Current overrides.allow match on past would-blocks — not a changelog of new additions.
|
|
191
|
-
const allowPatterns = options.allowPatterns ?? [];
|
|
192
|
-
for (const record of classifierAsks) {
|
|
193
|
-
if (!record.fingerprint) {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const command = commandFromRecord(record);
|
|
197
|
-
if (!command) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
for (const pattern of allowPatterns) {
|
|
201
|
-
if (matchesCustomCommand(command, command, pattern)) {
|
|
202
|
-
upsertCandidate(map, {
|
|
203
|
-
fingerprint: record.fingerprint,
|
|
204
|
-
command,
|
|
205
|
-
reason: record.reason ?? 'unknown',
|
|
206
|
-
source: 'overrides_allow',
|
|
207
|
-
});
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
189
|
return [...map.values()].sort((left, right) => {
|
|
213
190
|
if (right.askCount !== left.askCount) {
|
|
214
191
|
return right.askCount - left.askCount;
|
|
@@ -216,11 +193,11 @@ export function extractHarvestCandidates(records, options = {}) {
|
|
|
216
193
|
return left.command.localeCompare(right.command);
|
|
217
194
|
});
|
|
218
195
|
}
|
|
219
|
-
export function buildHarvestReport(records
|
|
196
|
+
export function buildHarvestReport(records) {
|
|
220
197
|
return {
|
|
221
198
|
schemaVersion: HARVEST_REPORT_SCHEMA_VERSION,
|
|
222
199
|
scope: 'shell',
|
|
223
|
-
candidates: extractHarvestCandidates(records
|
|
200
|
+
candidates: extractHarvestCandidates(records),
|
|
224
201
|
availabilityQueue: extractAvailabilityQueue(records),
|
|
225
202
|
};
|
|
226
203
|
}
|
|
@@ -265,7 +242,7 @@ export function applyHarvestReview(cases, params) {
|
|
|
265
242
|
...(reason ? { reason } : {}),
|
|
266
243
|
};
|
|
267
244
|
const followUp = category === 'provably-benign'
|
|
268
|
-
? 'Next: run `pnpm corpus` and confirm hard gates pass
|
|
245
|
+
? 'Next: run `pnpm corpus` and confirm the CI-only hard gates pass. Corpus labels never grant runtime shell authority.'
|
|
269
246
|
: 'Next: run `pnpm corpus` to verify corpus evaluation (accepted-benign is soft-gated).';
|
|
270
247
|
return {
|
|
271
248
|
cases: [...cases, nextCase],
|
package/dist/core/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, t
|
|
|
14
14
|
export type { GatedAction, GatedActionKind, GatePermissionResponse, GateVerdict, } from './gate-contract.js';
|
|
15
15
|
export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
|
|
16
16
|
export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
|
|
17
|
+
export type { GitResourceIdentity, GitResourceIdentityInspection, } from './git-resource-identity.js';
|
|
18
|
+
export { inspectGitResourceIdentity, isGitMetadataPath, resolveGitResourceIdentity, sameGitResourceIdentity, } from './git-resource-identity.js';
|
|
17
19
|
export { matchesSensitivePath } from './glob.js';
|
|
18
20
|
export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
|
|
19
21
|
export type { RecoveryBackend, RecoveryCheckpointEntry, RecoveryCheckpointEntryV1, RecoveryCheckpointEntryV2, RecoveryCheckpointManifest, RecoveryCheckpointManifestV1, RecoveryCheckpointManifestV2, RecoveryCheckpointState, RecoveryCheckpointSummary, RecoveryFileSnapshotV1, RecoveryFileSnapshotV2, RecoveryProofV1, RecoveryReceiptV1, } from './recovery/index.js';
|
package/dist/core/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { matchesCustomCommand } from './custom-command-match.js';
|
|
|
10
10
|
export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
|
|
11
11
|
export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
|
|
12
12
|
export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
|
|
13
|
+
export { inspectGitResourceIdentity, isGitMetadataPath, resolveGitResourceIdentity, sameGitResourceIdentity, } from './git-resource-identity.js';
|
|
13
14
|
export { matchesSensitivePath } from './glob.js';
|
|
14
15
|
export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
|
|
15
16
|
export { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './recovery/index.js';
|
package/dist/core/path-utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, realpathSync
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { inspectGitResourceIdentity } from './git-resource-identity.js';
|
|
3
4
|
import { isFdDuplication, isRedirectOperator } from './shell-tokenizer.js';
|
|
4
5
|
/**
|
|
5
6
|
* Resolve symlinks for the longest existing prefix of `targetPath`, then append
|
|
@@ -65,9 +66,42 @@ export function relativeWithinRepo(repoRoot, targetPath) {
|
|
|
65
66
|
}
|
|
66
67
|
export function resolveWorkspaceRootMatch(repoRoot, trustedRoots = [], targetPath) {
|
|
67
68
|
const canonicalRepoRoot = canonicalPath(repoRoot);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const repoInspection = inspectGitResourceIdentity(repoRoot);
|
|
70
|
+
const targetInspection = inspectGitResourceIdentity(targetPath);
|
|
71
|
+
if (repoInspection.status === 'resolved') {
|
|
72
|
+
if (targetInspection.status === 'resolved') {
|
|
73
|
+
if (repoInspection.identity.commonDir === targetInspection.identity.commonDir) {
|
|
74
|
+
const relativePath = relativeWithinRoot(targetInspection.identity.repositoryRoot, targetPath);
|
|
75
|
+
if (relativePath !== null) {
|
|
76
|
+
return {
|
|
77
|
+
kind: 'repo',
|
|
78
|
+
root: targetInspection.identity.repositoryRoot,
|
|
79
|
+
relativePath,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (targetInspection.status === 'invalid') {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (relativeWithinRoot(repoRoot, targetPath) !== null) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (repoInspection.status === 'invalid') {
|
|
93
|
+
if (relativeWithinRoot(repoRoot, targetPath) !== null || targetInspection.status !== 'absent') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (targetInspection.status === 'absent') {
|
|
98
|
+
const repoRelative = relativeWithinRoot(repoRoot, targetPath);
|
|
99
|
+
if (repoRelative !== null) {
|
|
100
|
+
return { kind: 'repo', root: canonicalRepoRoot, relativePath: repoRelative };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
return null;
|
|
71
105
|
}
|
|
72
106
|
const normalizedTrustedRoots = [...new Set(trustedRoots.map((root) => path.resolve(root)))]
|
|
73
107
|
.filter((root) => root !== canonicalRepoRoot)
|
|
@@ -137,24 +171,9 @@ export function hasOutsideRepoPath(tokens, cwd, repoRoot) {
|
|
|
137
171
|
});
|
|
138
172
|
}
|
|
139
173
|
export function containingGitRoot(targetPath) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!statSync(current).isDirectory()) {
|
|
144
|
-
current = path.dirname(current);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
current = path.dirname(current);
|
|
149
|
-
}
|
|
150
|
-
while (true) {
|
|
151
|
-
if (existsSync(path.join(current, '.git'))) {
|
|
152
|
-
return current;
|
|
153
|
-
}
|
|
154
|
-
const parent = path.dirname(current);
|
|
155
|
-
if (parent === current) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
current = parent;
|
|
174
|
+
const inspection = inspectGitResourceIdentity(targetPath);
|
|
175
|
+
if (inspection.status === 'resolved') {
|
|
176
|
+
return inspection.identity.repositoryRoot;
|
|
159
177
|
}
|
|
178
|
+
return inspection.status === 'invalid' ? inspection.boundaryPath : null;
|
|
160
179
|
}
|
|
@@ -4,3 +4,4 @@ export { MAX_SUBSTITUTION_DEPTH };
|
|
|
4
4
|
* Finds inner commands for $(...) and backtick substitution, respecting escapes and nesting.
|
|
5
5
|
*/
|
|
6
6
|
export declare function findCommandSubstitutions(command: string): string[];
|
|
7
|
+
export declare function findStructuralCommandSubstitutions(command: string): string[];
|