mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
import { getToolInputPath } from "#src/access-intent/tool-input-path";
|
|
import type { PathNormalizer } from "#src/path-normalizer";
|
|
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
|
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
|
import { SessionApproval } from "#src/session-approval";
|
|
import { deriveApprovalPattern } from "#src/session-rules";
|
|
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
|
import type { GateDescriptor, GateResult } from "./descriptor";
|
|
import { accessFactsFromPath } from "./helpers";
|
|
import type { ToolCallContext } from "./types";
|
|
|
|
/**
|
|
* Build a pure descriptor for the cross-cutting path permission gate (tools).
|
|
*
|
|
* Returns `null` when the gate does not apply (tool is not path-bearing,
|
|
* no extractable path, the `path` surface evaluates to `allow`, or no
|
|
* explicit `path` rule matched — i.e. only the universal default fired).
|
|
* Returns a `GateDescriptor` when the path matches a `deny` or `ask` rule.
|
|
*/
|
|
export function describePathGate(
|
|
tcc: ToolCallContext,
|
|
resolver: ScopedPermissionResolver,
|
|
normalizer: PathNormalizer,
|
|
extractors?: ToolAccessExtractorLookup,
|
|
): GateResult {
|
|
const filePath = getToolInputPath(tcc.toolName, tcc.input, extractors);
|
|
if (!filePath) return null;
|
|
|
|
// Emit an access-path intent so the resolver matches the lexical aliases
|
|
// *and* the canonical (symlink-resolved) form, the same set
|
|
// `external_directory` matches (#418, #486).
|
|
const accessPath = normalizer.forPath(filePath);
|
|
const check = resolver.resolve({
|
|
kind: "access-path",
|
|
surface: "path",
|
|
path: accessPath,
|
|
agentName: tcc.agentName ?? undefined,
|
|
});
|
|
|
|
if (check.state === "allow") return null;
|
|
|
|
// No explicit path rule matched — only the universal default fired.
|
|
// Skip the gate to preserve backward compatibility: configs without a
|
|
// "path" key should not trigger path-level prompts (#58).
|
|
if (check.matchedPattern === undefined) return null;
|
|
|
|
// Derive the approval pattern from the lexical absolute form so it matches
|
|
// the policy values a later call produces.
|
|
const pattern = deriveApprovalPattern(accessPath.value());
|
|
|
|
const payload = buildPathAskPayload({
|
|
toolName: tcc.toolName,
|
|
pathValue: filePath,
|
|
agentName: tcc.agentName,
|
|
matchedPattern: check.matchedPattern,
|
|
});
|
|
|
|
const descriptor: GateDescriptor = {
|
|
surface: "path",
|
|
input: { path: filePath },
|
|
payload,
|
|
sessionApproval: SessionApproval.single("path", pattern),
|
|
promptDetails: {
|
|
source: "tool_call",
|
|
agentName: tcc.agentName,
|
|
toolCallId: tcc.toolCallId,
|
|
toolName: tcc.toolName,
|
|
path: filePath,
|
|
accessIntent: accessFactsFromPath("path", accessPath),
|
|
},
|
|
logContext: {
|
|
source: "tool_call",
|
|
toolCallId: tcc.toolCallId,
|
|
toolName: tcc.toolName,
|
|
agentName: tcc.agentName,
|
|
path: filePath,
|
|
},
|
|
decision: {
|
|
surface: "path",
|
|
value: filePath,
|
|
},
|
|
preCheck: check,
|
|
};
|
|
|
|
return descriptor;
|
|
}
|