@guilz-dev/belay 0.9.2 → 0.9.4
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 +36 -2
- package/dist/adapters/codex/hooks.d.ts +1 -0
- package/dist/adapters/codex/hooks.js +3 -0
- package/dist/adapters/codex/runtime-entry.d.ts +3 -0
- package/dist/adapters/codex/runtime-entry.js +27 -4
- package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
- package/dist/adapters/cursor/cwd-resolution.js +58 -0
- package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
- package/dist/adapters/cursor/dispatcher-generation.js +5 -0
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +7 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
- package/dist/adapters/cursor/hook-router.d.ts +23 -0
- package/dist/adapters/cursor/hook-router.js +266 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -0
- package/dist/adapters/cursor/hooks.js +115 -3
- package/dist/adapters/cursor/routing-layout.d.ts +5 -0
- package/dist/adapters/cursor/routing-layout.js +29 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +18 -0
- package/dist/adapters/cursor/runtime-entry.js +135 -34
- package/dist/adapters/layouts/scope.d.ts +5 -0
- package/dist/adapters/layouts/scope.js +71 -0
- package/dist/adapters/shared/gate-runtime.js +27 -3
- package/dist/adapters/shared/repo-root.js +20 -1
- package/dist/bundle/claude-runtime.mjs +2660 -1951
- package/dist/bundle/codex-runtime.mjs +2680 -1953
- package/dist/bundle/cursor-dispatcher.mjs +443 -0
- package/dist/bundle/cursor-runtime.mjs +5638 -4697
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +215 -16
- package/dist/commands/health-snapshot.d.ts +3 -0
- package/dist/commands/health-snapshot.js +61 -0
- package/dist/commands/recovery-checkpoints.d.ts +3 -3
- package/dist/commands/report.js +14 -0
- package/dist/commands/status.js +15 -0
- package/dist/commands/where.d.ts +4 -0
- package/dist/commands/where.js +52 -0
- package/dist/config-io.js +11 -2
- package/dist/core/approval-repo-lookup.d.ts +16 -0
- package/dist/core/approval-repo-lookup.js +48 -0
- package/dist/core/audit-io.d.ts +1 -1
- package/dist/core/audit-io.js +1 -1
- package/dist/core/audit-legacy-archive.js +43 -25
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +7 -0
- package/dist/core/audit-serialize.d.ts +3 -0
- package/dist/core/audit-serialize.js +39 -3
- package/dist/core/audit-summary.d.ts +9 -0
- package/dist/core/audit-summary.js +58 -1
- package/dist/core/audit-types.d.ts +4 -0
- package/dist/core/classify-tool.js +16 -8
- package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
- package/dist/core/effect-ir/argv-delegate.js +42 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
- package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/augment.js +97 -0
- package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
- package/dist/core/effect-ir/shell-lower/context.js +1 -0
- package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +29 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
- package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
- package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
- package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/segment.js +82 -0
- package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
- package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
- package/dist/core/effect-ir/shell-lower.d.ts +2 -7
- package/dist/core/effect-ir/shell-lower.js +122 -1511
- package/dist/core/glob.js +32 -16
- package/dist/core/integrity.d.ts +2 -2
- package/dist/core/integrity.js +52 -12
- package/dist/core/replay-scrub.d.ts +3 -0
- package/dist/core/replay-scrub.js +30 -3
- package/dist/core/shell-substitution.js +2 -2
- package/dist/core/shell-tokenizer.d.ts +28 -0
- package/dist/core/shell-tokenizer.js +111 -29
- package/dist/core/shell-unparseable.js +3 -1
- package/dist/core/verdict/docker-compose-run.d.ts +18 -0
- package/dist/core/verdict/docker-compose-run.js +136 -0
- package/dist/core/verdict/launcher-resolve.js +28 -28
- package/dist/core/verdict/parser.d.ts +3 -0
- package/dist/core/verdict/parser.js +23 -40
- package/dist/core/verdict/recursive-invocation.d.ts +20 -0
- package/dist/core/verdict/recursive-invocation.js +224 -0
- package/dist/defaults.d.ts +7 -3
- package/dist/defaults.js +35 -21
- package/dist/installer/bootstrap.d.ts +1 -0
- package/dist/installer/bootstrap.js +2 -2
- package/dist/installer/runtime-artifacts.js +43 -13
- package/dist/installer/scope-config.d.ts +2 -2
- package/dist/installer.d.ts +10 -1
- package/dist/installer.js +83 -7
- package/dist/node-resolution.d.ts +1 -0
- package/dist/node-resolution.js +61 -0
- package/dist/templates.d.ts +7 -4
- package/dist/templates.js +42 -4
- package/dist/types.d.ts +35 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +5 -0
- package/skills/belay/belay-report.md +4 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AdapterName } from '../adapters/layouts/types.js';
|
|
2
|
+
export type ApprovalRepoLookupResult = {
|
|
3
|
+
status: 'found';
|
|
4
|
+
repoRoot: string;
|
|
5
|
+
} | {
|
|
6
|
+
status: 'ambiguous';
|
|
7
|
+
repoRoots: string[];
|
|
8
|
+
} | {
|
|
9
|
+
status: 'not_found';
|
|
10
|
+
};
|
|
11
|
+
export declare function findApprovalRepoRoots(params: {
|
|
12
|
+
approvalId: string;
|
|
13
|
+
candidateRepoRoots: string[];
|
|
14
|
+
adapter: AdapterName;
|
|
15
|
+
}): Promise<ApprovalRepoLookupResult>;
|
|
16
|
+
export declare function formatAmbiguousApprovalRepoMessage(repoRoots: string[]): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { approvedApprovalsPath, loadConfigFile, pendingApprovalsPath } from '../config-io.js';
|
|
4
|
+
async function approvalStateContainsId(filePath, approvalId) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = await readFile(filePath, 'utf8');
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
return parsed.approvals?.some((entry) => entry.approvalId === approvalId) ?? false;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function findApprovalRepoRoots(params) {
|
|
15
|
+
const matches = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for (const candidate of params.candidateRepoRoots) {
|
|
18
|
+
const repoRoot = path.resolve(candidate);
|
|
19
|
+
if (seen.has(repoRoot)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
seen.add(repoRoot);
|
|
23
|
+
try {
|
|
24
|
+
const config = await loadConfigFile(repoRoot, params.adapter);
|
|
25
|
+
const pendingPath = pendingApprovalsPath(repoRoot, config);
|
|
26
|
+
const approvedPath = approvedApprovalsPath(repoRoot, config);
|
|
27
|
+
const hasPending = await approvalStateContainsId(pendingPath, params.approvalId);
|
|
28
|
+
const hasApproved = await approvalStateContainsId(approvedPath, params.approvalId);
|
|
29
|
+
if (hasPending || hasApproved) {
|
|
30
|
+
matches.push(repoRoot);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (matches.length === 1) {
|
|
38
|
+
return { status: 'found', repoRoot: matches[0] };
|
|
39
|
+
}
|
|
40
|
+
if (matches.length > 1) {
|
|
41
|
+
return { status: 'ambiguous', repoRoots: matches };
|
|
42
|
+
}
|
|
43
|
+
return { status: 'not_found' };
|
|
44
|
+
}
|
|
45
|
+
export function formatAmbiguousApprovalRepoMessage(repoRoots) {
|
|
46
|
+
return ('Belay found the same approval ID in multiple repositories. ' +
|
|
47
|
+
`Open one workspace and retry: ${repoRoots.join(', ')}`);
|
|
48
|
+
}
|
package/dist/core/audit-io.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from './config.js';
|
|
2
|
-
export { AUDIT_SCHEMA_VERSION, appendAuditRecord, approvalCorrelationId, isValidAuditFingerprint, isValidAuditTimestamp, parseAuditNdjsonLine, serializeAuditRecordV3, } from './audit-serialize.js';
|
|
2
|
+
export { AUDIT_SCHEMA_VERSION, appendAuditRecord, approvalCorrelationId, canonicalToolUseIdForCorrelation, isValidAuditFingerprint, isValidAuditTimestamp, parseAuditNdjsonLine, serializeAuditRecordV3, toolInvocationCorrelationId, } from './audit-serialize.js';
|
|
3
3
|
export declare function appendCliAuditEvent(repoRoot: string, config: BelayConfigV4, event: Record<string, unknown>): Promise<void>;
|
package/dist/core/audit-io.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { resolveActiveAuditCohort } from '../runtime-provenance.js';
|
|
3
3
|
import { appendAuditRecord } from './audit-serialize.js';
|
|
4
4
|
import { scrubOptionsFromConfig } from './config.js';
|
|
5
|
-
export { AUDIT_SCHEMA_VERSION, appendAuditRecord, approvalCorrelationId, isValidAuditFingerprint, isValidAuditTimestamp, parseAuditNdjsonLine, serializeAuditRecordV3, } from './audit-serialize.js';
|
|
5
|
+
export { AUDIT_SCHEMA_VERSION, appendAuditRecord, approvalCorrelationId, canonicalToolUseIdForCorrelation, isValidAuditFingerprint, isValidAuditTimestamp, parseAuditNdjsonLine, serializeAuditRecordV3, toolInvocationCorrelationId, } from './audit-serialize.js';
|
|
6
6
|
export async function appendCliAuditEvent(repoRoot, config, event) {
|
|
7
7
|
const auditPath = path.isAbsolute(config.audit.logPath)
|
|
8
8
|
? config.audit.logPath
|
|
@@ -1,43 +1,61 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { createReadStream, existsSync } from 'node:fs';
|
|
2
|
+
import { rename } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
4
5
|
const LEGACY_PLACEHOLDER_PATTERNS = [
|
|
5
6
|
/"(?:timestamp|ts)"\s*:\s*"<timestamp>"/,
|
|
6
7
|
/"(?:fingerprint|commandFingerprint|effectIRHash|payloadHash|configFingerprint|runtimeArtifactHash|decisionConfigFingerprint|receiptHash)"\s*:\s*"<high-entropy>"/,
|
|
7
8
|
/"approvalId"\s*:\s*"<approval-id>"/,
|
|
8
9
|
];
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
const LEGACY_HIGH_ENTROPY_FIELDS = [
|
|
11
|
+
'fingerprint',
|
|
12
|
+
'commandFingerprint',
|
|
13
|
+
'effectIRHash',
|
|
14
|
+
'payloadHash',
|
|
15
|
+
'configFingerprint',
|
|
16
|
+
'runtimeArtifactHash',
|
|
17
|
+
'decisionConfigFingerprint',
|
|
18
|
+
'receiptHash',
|
|
19
|
+
];
|
|
11
20
|
export function auditLogHasLegacyScrubPlaceholders(sample) {
|
|
12
|
-
|
|
21
|
+
for (const line of sample.split(/\r?\n/)) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(trimmed);
|
|
28
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
29
|
+
if (auditRecordHasLegacyCorrelationPlaceholders(parsed)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Malformed legacy lines still need archival before a new runtime appends to the log.
|
|
37
|
+
}
|
|
38
|
+
if (LEGACY_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
13
43
|
}
|
|
14
44
|
export function auditRecordHasLegacyCorrelationPlaceholders(record) {
|
|
15
45
|
return (record.timestamp === '<timestamp>' ||
|
|
16
|
-
record.
|
|
46
|
+
record.ts === '<timestamp>' ||
|
|
47
|
+
LEGACY_HIGH_ENTROPY_FIELDS.some((field) => record[field] === '<high-entropy>') ||
|
|
17
48
|
record.approvalId === '<approval-id>');
|
|
18
49
|
}
|
|
19
50
|
async function auditFileHasLegacyScrubPlaceholders(auditPath) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
while (true) {
|
|
26
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
27
|
-
if (bytesRead === 0) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
const sample = carry + buffer.toString('utf8', 0, bytesRead);
|
|
31
|
-
if (auditLogHasLegacyScrubPlaceholders(sample)) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
carry = sample.slice(-AUDIT_SCAN_OVERLAP_CHARS);
|
|
35
|
-
position += bytesRead;
|
|
51
|
+
const input = createReadStream(auditPath, { encoding: 'utf8' });
|
|
52
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
53
|
+
for await (const line of lines) {
|
|
54
|
+
if (auditLogHasLegacyScrubPlaceholders(line)) {
|
|
55
|
+
return true;
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
|
-
|
|
39
|
-
await handle.close();
|
|
40
|
-
}
|
|
58
|
+
return false;
|
|
41
59
|
}
|
|
42
60
|
export async function archiveLegacyAuditLogIfNeeded(repoRoot, config) {
|
|
43
61
|
const auditPath = path.isAbsolute(config.audit.logPath)
|
|
@@ -3,6 +3,7 @@ export declare function toAuditRecord(value: Record<string, unknown>): AuditReco
|
|
|
3
3
|
export declare function parseTimestamp(value?: string): number | null;
|
|
4
4
|
export declare function auditFingerprint(record: AuditRecord): string | undefined;
|
|
5
5
|
export declare function auditApprovalCorrelationId(record: AuditRecord): string | undefined;
|
|
6
|
+
export declare function auditToolInvocationCorrelationId(record: AuditRecord): string | undefined;
|
|
6
7
|
export declare function isGateRecord(record: AuditRecord): boolean;
|
|
7
8
|
export declare function isShellGateRecord(record: AuditRecord): boolean;
|
|
8
9
|
export declare function isApprovalRecorded(record: AuditRecord): boolean;
|
package/dist/core/audit-query.js
CHANGED
|
@@ -27,6 +27,13 @@ export function auditApprovalCorrelationId(record) {
|
|
|
27
27
|
}
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
30
|
+
export function auditToolInvocationCorrelationId(record) {
|
|
31
|
+
if (typeof record.toolInvocationCorrelationId === 'string' &&
|
|
32
|
+
isValidApprovalCorrelationId(record.toolInvocationCorrelationId)) {
|
|
33
|
+
return record.toolInvocationCorrelationId;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
30
37
|
export function isGateRecord(record) {
|
|
31
38
|
return typeof record.event === 'string' && GATE_EVENTS.has(record.event);
|
|
32
39
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ScrubOptions } from './types.js';
|
|
2
2
|
export declare const AUDIT_SCHEMA_VERSION = 3;
|
|
3
3
|
export declare function approvalCorrelationId(approvalId: string): string;
|
|
4
|
+
/** Normalize Cursor host tool_use_id values before correlation hashing. */
|
|
5
|
+
export declare function canonicalToolUseIdForCorrelation(toolUseId: string): string;
|
|
6
|
+
export declare function toolInvocationCorrelationId(toolUseId: string): string;
|
|
4
7
|
export declare function isValidApprovalCorrelationId(value: string): boolean;
|
|
5
8
|
export declare function isValidAuditTimestamp(value: string): boolean;
|
|
6
9
|
export declare function isValidAuditFingerprint(value: string): boolean;
|
|
@@ -19,6 +19,7 @@ const PRESERVED_HASH_FIELDS = new Set([
|
|
|
19
19
|
const PRESERVED_LITERAL_FIELDS = new Set([
|
|
20
20
|
'timestamp',
|
|
21
21
|
'approvalCorrelationId',
|
|
22
|
+
'toolInvocationCorrelationId',
|
|
22
23
|
'runtimeVersion',
|
|
23
24
|
'runtimeBuildStamp',
|
|
24
25
|
'boundaryProfile',
|
|
@@ -42,6 +43,27 @@ const SCRUBBED_CONTAINER_FIELDS = new Set([
|
|
|
42
43
|
export function approvalCorrelationId(approvalId) {
|
|
43
44
|
return createHash('sha256').update(approvalId).digest('hex').slice(0, 16);
|
|
44
45
|
}
|
|
46
|
+
const TOOL_USE_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
47
|
+
/** Normalize Cursor host tool_use_id values before correlation hashing. */
|
|
48
|
+
export function canonicalToolUseIdForCorrelation(toolUseId) {
|
|
49
|
+
const trimmed = toolUseId.trim();
|
|
50
|
+
if (trimmed.startsWith('tool_')) {
|
|
51
|
+
const remainder = trimmed.slice('tool_'.length);
|
|
52
|
+
if (TOOL_USE_UUID_PATTERN.test(remainder)) {
|
|
53
|
+
return remainder.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (TOOL_USE_UUID_PATTERN.test(trimmed)) {
|
|
57
|
+
return trimmed.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
export function toolInvocationCorrelationId(toolUseId) {
|
|
62
|
+
return createHash('sha256')
|
|
63
|
+
.update(canonicalToolUseIdForCorrelation(toolUseId))
|
|
64
|
+
.digest('hex')
|
|
65
|
+
.slice(0, 16);
|
|
66
|
+
}
|
|
45
67
|
export function isValidApprovalCorrelationId(value) {
|
|
46
68
|
return /^[a-f0-9]{16}$/.test(value);
|
|
47
69
|
}
|
|
@@ -64,7 +86,17 @@ function isValidPreservedHashField(field, value) {
|
|
|
64
86
|
return isValidAuditFingerprint(value);
|
|
65
87
|
}
|
|
66
88
|
function scrubAuditContainer(value, options) {
|
|
67
|
-
|
|
89
|
+
const withoutRawToolIds = (input) => {
|
|
90
|
+
if (Array.isArray(input))
|
|
91
|
+
return input.map(withoutRawToolIds);
|
|
92
|
+
if (input && typeof input === 'object') {
|
|
93
|
+
return Object.fromEntries(Object.entries(input)
|
|
94
|
+
.filter(([key]) => key !== 'tool_use_id')
|
|
95
|
+
.map(([key, child]) => [key, withoutRawToolIds(child)]));
|
|
96
|
+
}
|
|
97
|
+
return input;
|
|
98
|
+
};
|
|
99
|
+
return scrubValue(withoutRawToolIds(value), {
|
|
68
100
|
...options,
|
|
69
101
|
maskHighEntropyStrings: true,
|
|
70
102
|
});
|
|
@@ -83,7 +115,7 @@ function serializeAuditField(key, value, options) {
|
|
|
83
115
|
if (key === 'timestamp' && typeof value === 'string' && isValidAuditTimestamp(value)) {
|
|
84
116
|
return value;
|
|
85
117
|
}
|
|
86
|
-
if (key === 'approvalCorrelationId' &&
|
|
118
|
+
if ((key === 'approvalCorrelationId' || key === 'toolInvocationCorrelationId') &&
|
|
87
119
|
typeof value === 'string' &&
|
|
88
120
|
isValidApprovalCorrelationId(value)) {
|
|
89
121
|
return value;
|
|
@@ -145,7 +177,11 @@ export function serializeAuditRecordV3(record, options) {
|
|
|
145
177
|
serialized.approvalCorrelationId = record.approvalCorrelationId;
|
|
146
178
|
}
|
|
147
179
|
for (const [key, value] of Object.entries(record)) {
|
|
148
|
-
if (key === 'timestamp' ||
|
|
180
|
+
if (key === 'timestamp' ||
|
|
181
|
+
key === 'ts' ||
|
|
182
|
+
key === 'approvalId' ||
|
|
183
|
+
key === 'tool_use_id' ||
|
|
184
|
+
key === 'schemaVersion') {
|
|
149
185
|
continue;
|
|
150
186
|
}
|
|
151
187
|
const next = serializeAuditField(key, value, options);
|
|
@@ -6,6 +6,12 @@ export interface RecentAskEntry {
|
|
|
6
6
|
reason: string;
|
|
7
7
|
tier: AuditTier;
|
|
8
8
|
}
|
|
9
|
+
export interface RecentHostDenialEntry {
|
|
10
|
+
gateTimestamp?: string;
|
|
11
|
+
failureTimestamp?: string;
|
|
12
|
+
summary: string;
|
|
13
|
+
errorMessage: string;
|
|
14
|
+
}
|
|
9
15
|
export interface AuditVisibilitySummary {
|
|
10
16
|
gateEvents: number;
|
|
11
17
|
askCount: number;
|
|
@@ -16,6 +22,9 @@ export interface AuditVisibilitySummary {
|
|
|
16
22
|
allowCount: number;
|
|
17
23
|
silentPassRate: number;
|
|
18
24
|
recentAsks: RecentAskEntry[];
|
|
25
|
+
hostDeniedAfterAllowCount: number;
|
|
26
|
+
recentHostDenials: RecentHostDenialEntry[];
|
|
27
|
+
unrecognizedHostFailureCount: number;
|
|
19
28
|
}
|
|
20
29
|
export declare const DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
|
|
21
30
|
export declare const MIN_GATE_EVENTS_FOR_FENCE_DRIFT = 20;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { filterAuditRecords, inferWouldBlock, isApprovalRecorded, isGateRecord, parseTimestamp, recordStringField, } from './audit-query.js';
|
|
1
|
+
import { auditToolInvocationCorrelationId, filterAuditRecords, inferWouldBlock, isApprovalRecorded, isGateRecord, parseTimestamp, recordStringField, } from './audit-query.js';
|
|
2
2
|
export const DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
|
|
3
3
|
export const MIN_GATE_EVENTS_FOR_FENCE_DRIFT = 20;
|
|
4
4
|
function isTier0Reason(reason) {
|
|
@@ -38,6 +38,7 @@ function isGateEventRecord(record) {
|
|
|
38
38
|
export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
39
39
|
const filtered = filterAuditRecords(records, filter);
|
|
40
40
|
const gateRecords = filtered.filter(isGateEventRecord);
|
|
41
|
+
const allGateRecords = records.filter(isGateEventRecord);
|
|
41
42
|
const recentAskLimit = options.recentAskLimit ?? 10;
|
|
42
43
|
let askCount = 0;
|
|
43
44
|
let enforceAskCount = 0;
|
|
@@ -46,6 +47,23 @@ export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
|
46
47
|
let flagCount = 0;
|
|
47
48
|
let allowCount = 0;
|
|
48
49
|
const recentAsks = [];
|
|
50
|
+
const allowedByInvocation = new Map();
|
|
51
|
+
const recentHostDenials = [];
|
|
52
|
+
let unrecognizedHostFailureCount = 0;
|
|
53
|
+
const matchedHostDenials = new Set();
|
|
54
|
+
for (const record of allGateRecords) {
|
|
55
|
+
const invocationId = auditToolInvocationCorrelationId(record);
|
|
56
|
+
if (invocationId && !inferWouldBlock(record) && record.permission === 'allow') {
|
|
57
|
+
const existing = allowedByInvocation.get(invocationId);
|
|
58
|
+
const recordMs = parseTimestamp(record.timestamp) ?? Number.NEGATIVE_INFINITY;
|
|
59
|
+
const existingMs = existing
|
|
60
|
+
? (parseTimestamp(existing.timestamp) ?? Number.NEGATIVE_INFINITY)
|
|
61
|
+
: Number.NEGATIVE_INFINITY;
|
|
62
|
+
if (!existing || recordMs >= existingMs) {
|
|
63
|
+
allowedByInvocation.set(invocationId, record);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
49
67
|
for (const record of gateRecords) {
|
|
50
68
|
if (inferWouldBlock(record)) {
|
|
51
69
|
askCount += 1;
|
|
@@ -73,11 +91,47 @@ export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
|
73
91
|
allowCount += 1;
|
|
74
92
|
}
|
|
75
93
|
}
|
|
94
|
+
for (const record of filtered) {
|
|
95
|
+
if (record.event !== 'postToolUseFailure') {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (record.failureType !== 'permission_denied') {
|
|
99
|
+
unrecognizedHostFailureCount += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const invocationId = auditToolInvocationCorrelationId(record);
|
|
103
|
+
const gate = invocationId ? allowedByInvocation.get(invocationId) : undefined;
|
|
104
|
+
if (!gate) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const gateMs = parseTimestamp(gate.timestamp);
|
|
108
|
+
const failureMs = parseTimestamp(record.timestamp);
|
|
109
|
+
if (gateMs !== null && failureMs !== null && failureMs < gateMs) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (invocationId && matchedHostDenials.has(invocationId)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (invocationId) {
|
|
116
|
+
matchedHostDenials.add(invocationId);
|
|
117
|
+
}
|
|
118
|
+
recentHostDenials.push({
|
|
119
|
+
gateTimestamp: gate.timestamp,
|
|
120
|
+
failureTimestamp: record.timestamp,
|
|
121
|
+
summary: typeof gate.summary === 'string' ? gate.summary : '',
|
|
122
|
+
errorMessage: typeof record.errorMessage === 'string' ? record.errorMessage : '',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
76
125
|
recentAsks.sort((left, right) => {
|
|
77
126
|
const leftMs = parseTimestamp(left.timestamp) ?? 0;
|
|
78
127
|
const rightMs = parseTimestamp(right.timestamp) ?? 0;
|
|
79
128
|
return rightMs - leftMs;
|
|
80
129
|
});
|
|
130
|
+
recentHostDenials.sort((left, right) => {
|
|
131
|
+
const leftMs = parseTimestamp(left.failureTimestamp) ?? 0;
|
|
132
|
+
const rightMs = parseTimestamp(right.failureTimestamp) ?? 0;
|
|
133
|
+
return rightMs - leftMs;
|
|
134
|
+
});
|
|
81
135
|
const gateEvents = gateRecords.length;
|
|
82
136
|
const silentPassRate = gateEvents > 0 ? (allowCount + flagCount) / gateEvents : 0;
|
|
83
137
|
return {
|
|
@@ -90,6 +144,9 @@ export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
|
90
144
|
allowCount,
|
|
91
145
|
silentPassRate,
|
|
92
146
|
recentAsks: recentAsks.slice(0, recentAskLimit),
|
|
147
|
+
hostDeniedAfterAllowCount: recentHostDenials.length,
|
|
148
|
+
recentHostDenials: recentHostDenials.slice(0, recentAskLimit),
|
|
149
|
+
unrecognizedHostFailureCount,
|
|
93
150
|
};
|
|
94
151
|
}
|
|
95
152
|
export function detectFenceDrift(summary, options = {}) {
|
|
@@ -12,6 +12,10 @@ export interface AuditRecord {
|
|
|
12
12
|
fingerprint?: string;
|
|
13
13
|
summary?: string;
|
|
14
14
|
approvalId?: string;
|
|
15
|
+
toolInvocationCorrelationId?: string;
|
|
16
|
+
toolName?: string;
|
|
17
|
+
failureType?: string;
|
|
18
|
+
errorMessage?: string;
|
|
15
19
|
wouldBlock?: boolean;
|
|
16
20
|
judgeFallbackReason?: string;
|
|
17
21
|
permission?: string;
|
|
@@ -5,6 +5,7 @@ import { evaluateFileMutationPolicy, policyDecisionRequiresAsk, } from './capabi
|
|
|
5
5
|
import { canonicalStringify, toolFingerprint } from './fingerprint.js';
|
|
6
6
|
import { matchesSensitivePath } from './glob.js';
|
|
7
7
|
import { pathWithinRoot, resolveWorkspaceRootMatch } from './path-utils.js';
|
|
8
|
+
import { fingerprintToolInputSource } from './replay-scrub.js';
|
|
8
9
|
import { scrubValue } from './scrub.js';
|
|
9
10
|
import { classifyShell, resolveClassifierTrustedCwd } from './verdict/adapter.js';
|
|
10
11
|
import { isGitPath } from './verdict/containment.js';
|
|
@@ -37,6 +38,13 @@ function policyAuth(options) {
|
|
|
37
38
|
function scrubPayload(value, options) {
|
|
38
39
|
return scrubValue(value, options.scrubOptions);
|
|
39
40
|
}
|
|
41
|
+
function fingerprintPayload(payload, options) {
|
|
42
|
+
const toolInput = fingerprintToolInputSource(payload);
|
|
43
|
+
if (Object.keys(toolInput).length > 0) {
|
|
44
|
+
return toolInput;
|
|
45
|
+
}
|
|
46
|
+
return scrubPayload(payload.tool_input ?? {}, options);
|
|
47
|
+
}
|
|
40
48
|
function extractFilePath(payload) {
|
|
41
49
|
const toolInput = payload.tool_input;
|
|
42
50
|
if (!toolInput || typeof toolInput !== 'object') {
|
|
@@ -209,7 +217,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
209
217
|
verdict: 'deny_pending_approval',
|
|
210
218
|
reason: 'tool_shell_missing_command',
|
|
211
219
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
212
|
-
fingerprint: toolFingerprint(toolName,
|
|
220
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
213
221
|
assessment: {
|
|
214
222
|
reversibility: 'irreversible',
|
|
215
223
|
external: false,
|
|
@@ -223,7 +231,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
223
231
|
verdict: 'allow_flagged',
|
|
224
232
|
reason: 'tool_shell_missing_command',
|
|
225
233
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
226
|
-
fingerprint: toolFingerprint(toolName,
|
|
234
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
227
235
|
assessment: {
|
|
228
236
|
reversibility: 'recoverable_with_cost',
|
|
229
237
|
external: false,
|
|
@@ -249,7 +257,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
249
257
|
verdict: 'deny_pending_approval',
|
|
250
258
|
reason: 'file_mutation_missing_path',
|
|
251
259
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
252
|
-
fingerprint: toolFingerprint(toolName,
|
|
260
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
253
261
|
assessment: {
|
|
254
262
|
reversibility: 'irreversible',
|
|
255
263
|
external: false,
|
|
@@ -263,7 +271,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
263
271
|
verdict: 'allow_flagged',
|
|
264
272
|
reason: 'file_mutation_missing_path',
|
|
265
273
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
266
|
-
fingerprint: toolFingerprint(toolName,
|
|
274
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
267
275
|
assessment: {
|
|
268
276
|
reversibility: 'recoverable_with_cost',
|
|
269
277
|
external: false,
|
|
@@ -409,7 +417,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
409
417
|
verdict: 'deny_pending_approval',
|
|
410
418
|
reason: 'apply_patch_missing_path',
|
|
411
419
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
412
|
-
fingerprint: toolFingerprint(toolName,
|
|
420
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
413
421
|
assessment: {
|
|
414
422
|
reversibility: 'irreversible',
|
|
415
423
|
external: false,
|
|
@@ -423,7 +431,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
423
431
|
verdict: 'allow_flagged',
|
|
424
432
|
reason: 'apply_patch_missing_path',
|
|
425
433
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
426
|
-
fingerprint: toolFingerprint(toolName,
|
|
434
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
427
435
|
assessment: {
|
|
428
436
|
reversibility: 'recoverable_with_cost',
|
|
429
437
|
external: false,
|
|
@@ -448,7 +456,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
448
456
|
verdict: 'allow_flagged',
|
|
449
457
|
reason: sawDelete ? 'file_delete' : 'file_mutation',
|
|
450
458
|
summary: targets.map((target) => target.path).join(', '),
|
|
451
|
-
fingerprint: toolFingerprint(toolName,
|
|
459
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
452
460
|
assessment: {
|
|
453
461
|
reversibility: 'recoverable_with_cost',
|
|
454
462
|
external: false,
|
|
@@ -462,7 +470,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
462
470
|
verdict: 'allow',
|
|
463
471
|
reason: 'unclassified_tool',
|
|
464
472
|
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
465
|
-
fingerprint: toolFingerprint(toolName,
|
|
473
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
466
474
|
assessment: {
|
|
467
475
|
reversibility: 'reversible',
|
|
468
476
|
external: false,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ArgvDelegatePeelResult {
|
|
2
|
+
launcher: string;
|
|
3
|
+
innerTokens: string[];
|
|
4
|
+
opaque: boolean;
|
|
5
|
+
reason: string;
|
|
6
|
+
signals: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function innerRecipeFromArgvDelegate(peel: ArgvDelegatePeelResult): string | null;
|
|
9
|
+
export declare function peelArgvDelegateArgv(tokens: string[]): ArgvDelegatePeelResult | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function innerRecipeFromArgvDelegate(peel) {
|
|
2
|
+
if (peel.opaque || peel.innerTokens.length === 0) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
return peel.innerTokens.join(' ');
|
|
6
|
+
}
|
|
7
|
+
export function peelArgvDelegateArgv(tokens) {
|
|
8
|
+
const launcher = tokens[0] ?? '';
|
|
9
|
+
if (!launcher || tokens.length < 2) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const signals = ['process.argv_delegate'];
|
|
13
|
+
let index = 1;
|
|
14
|
+
while (index < tokens.length) {
|
|
15
|
+
const token = tokens[index] ?? '';
|
|
16
|
+
if (token === '--') {
|
|
17
|
+
index += 1;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
if (token.startsWith('-')) {
|
|
21
|
+
return {
|
|
22
|
+
launcher,
|
|
23
|
+
innerTokens: [],
|
|
24
|
+
opaque: true,
|
|
25
|
+
reason: 'argv_delegate_wrapper_options',
|
|
26
|
+
signals: [...signals, 'argv_delegate_wrapper_options'],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const innerTokens = tokens.slice(index);
|
|
32
|
+
if (innerTokens.length === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
launcher,
|
|
37
|
+
innerTokens,
|
|
38
|
+
opaque: false,
|
|
39
|
+
reason: 'argv_delegate_peel',
|
|
40
|
+
signals,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ShellEffectRequirement } from '../shell-build.js';
|
|
2
|
+
export declare function shouldApplyArgvDelegate(head: string, innerTokens: string[], depth: number): boolean;
|
|
3
|
+
export declare function isGrammarUnknownOnly(requirements: ShellEffectRequirement[], head: string): boolean;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isEgressToolHead } from '../../verdict/egress-classify.js';
|
|
3
|
+
const ARGV_DELEGATE_INNER_BLOCKLIST = new Set([
|
|
4
|
+
'sudo',
|
|
5
|
+
'env',
|
|
6
|
+
'command',
|
|
7
|
+
'builtin',
|
|
8
|
+
'exec',
|
|
9
|
+
'time',
|
|
10
|
+
'nice',
|
|
11
|
+
'nohup',
|
|
12
|
+
'stdbuf',
|
|
13
|
+
'setsid',
|
|
14
|
+
'(',
|
|
15
|
+
]);
|
|
16
|
+
export function shouldApplyArgvDelegate(head, innerTokens, depth) {
|
|
17
|
+
if (depth > 0 || isEgressToolHead(head) || head === 'bundle') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (ARGV_DELEGATE_INNER_BLOCKLIST.has(head)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const innerHead = path.basename(innerTokens[0] ?? '');
|
|
24
|
+
if (ARGV_DELEGATE_INNER_BLOCKLIST.has(innerHead)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return innerTokens.length >= 2;
|
|
28
|
+
}
|
|
29
|
+
export function isGrammarUnknownOnly(requirements, head) {
|
|
30
|
+
if (requirements.length !== 2) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const [spawn, indeterminate] = requirements;
|
|
34
|
+
return (spawn.action === 'process.exec' &&
|
|
35
|
+
spawn.resource.kind === 'executable' &&
|
|
36
|
+
spawn.resource.command === head &&
|
|
37
|
+
spawn.resource.operation === 'spawn' &&
|
|
38
|
+
indeterminate.action === 'indeterminate' &&
|
|
39
|
+
indeterminate.evidence.signals.includes('process.grammar_unknown'));
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ShellEffectRequirement } from '../shell-build.js';
|
|
2
|
+
import type { LowerContext } from './context.js';
|
|
3
|
+
export declare function addRedirectEffects(requirements: ShellEffectRequirement[], tokens: string[], env: Readonly<Record<string, string | undefined>>, context: LowerContext, segment: string): void;
|
|
4
|
+
export declare function effectChangingEnvironmentSignals(head: string, env: Readonly<Record<string, string | undefined>>, changedNames: ReadonlySet<string>): string[];
|