mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 07:23:06 +00:00
216 lines
7.8 KiB
TypeScript
216 lines
7.8 KiB
TypeScript
import type { AccessPath } from "#src/access-intent/access-path";
|
||
import { BashProgram } from "#src/access-intent/bash/program";
|
||
import { getPathBearingToolPath } from "#src/access-intent/tool-input-path";
|
||
import {
|
||
resolveShellInvocation,
|
||
type ShellInvocation,
|
||
} from "#src/access-intent/tool-kind";
|
||
import type { ShellToolsConfig } from "#src/config-schema";
|
||
import type { PathNormalizer } from "#src/path-normalizer";
|
||
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
||
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
||
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
|
||
import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
|
||
import {
|
||
ToolPreviewFormatter,
|
||
type ToolPreviewFormatterOptions,
|
||
} from "#src/tool-preview-formatter";
|
||
import type { PermissionCheckResult } from "#src/types";
|
||
import { resolveBashCommandCheck } from "./bash-command";
|
||
import { describeBashExternalDirectoryGate } from "./bash-external-directory";
|
||
import { describeBashPathGate } from "./bash-path";
|
||
import type { GateResult } from "./descriptor";
|
||
import { describeExternalDirectoryGate } from "./external-directory";
|
||
import { describePathGate } from "./path";
|
||
import type { GateRunner } from "./runner";
|
||
import { describeSkillReadGate } from "./skill-read";
|
||
import { describeToolGate } from "./tool";
|
||
import { applyShellDecisionFloor } from "./shell-decision-floor";
|
||
import type { GateOutcome, ToolCallContext } from "./types";
|
||
|
||
/**
|
||
* Narrow interface the pipeline needs from its session-side dependency.
|
||
*
|
||
* The three query methods needed to assemble gate inputs.
|
||
* The resolver is injected separately as a constructor parameter.
|
||
*
|
||
* `PermissionSession` satisfies this structurally at the construction call
|
||
* site; no `implements` clause is needed and would create a layer-inversion
|
||
* import from the domain module into the handler layer.
|
||
*/
|
||
export interface ToolCallGateInputs {
|
||
/** Active skill prompt entries for the skill-read gate. */
|
||
getActiveSkillEntries(): SkillPromptEntry[];
|
||
/** Combined infrastructure read directories (static + config-derived). */
|
||
getInfrastructureReadDirs(): string[];
|
||
/** Resolved tool-preview formatter options from the current config. */
|
||
getToolPreviewLimits(): ToolPreviewFormatterOptions;
|
||
/** The session's path normalizer (platform + cwd baked in). */
|
||
getPathNormalizer(): PathNormalizer;
|
||
/**
|
||
* The configured shell-tool aliases (`shellTools`), or `undefined` when none
|
||
* are set. Consulted by {@link resolveShellInvocation} so an aliased shell
|
||
* tool is gated through the bash stack at parity with native `bash` (#574).
|
||
*/
|
||
getShellToolAliases(): ShellToolsConfig | undefined;
|
||
}
|
||
|
||
/**
|
||
* Owns the ordered tool-call gate-producer assembly and the run loop.
|
||
*
|
||
* Constructed once in the composition root and injected into
|
||
* `PermissionGateHandler`. `evaluate(tcc, runner)` encapsulates:
|
||
* - bash-command extraction and single `BashProgram.parse` (#308)
|
||
* - `ToolPreviewFormatter` construction from `getToolPreviewLimits()`
|
||
* - infrastructure-dir list from `getInfrastructureReadDirs()`
|
||
* - all six gate producers in their prescribed order
|
||
* - the run loop that returns the first block outcome, or allow
|
||
*/
|
||
export class ToolCallGatePipeline {
|
||
constructor(
|
||
private readonly resolver: ScopedPermissionResolver,
|
||
private readonly inputs: ToolCallGateInputs,
|
||
private readonly customFormatters?: ToolInputFormatterLookup,
|
||
private readonly customExtractors?: ToolAccessExtractorLookup,
|
||
) {}
|
||
|
||
async evaluate(
|
||
tcc: ToolCallContext,
|
||
runner: GateRunner,
|
||
): Promise<GateOutcome> {
|
||
// Resolve the shell invocation once: native `bash` and any tool recorded in
|
||
// `shellTools` both yield a command (+ optional workdir); every other tool
|
||
// yields null (#574). The three bash gates then share the single BashProgram
|
||
// parsed from that command instead of each re-parsing (#308).
|
||
const shell = resolveShellInvocation(
|
||
tcc.toolName,
|
||
tcc.input,
|
||
this.inputs.getShellToolAliases(),
|
||
);
|
||
const normalizer = this.inputs.getPathNormalizer();
|
||
const bashProgram = shell?.command
|
||
? await BashProgram.parse(shell.command, normalizer, {
|
||
workdir: shell.workdir,
|
||
})
|
||
: null;
|
||
|
||
const formatter = new ToolPreviewFormatter(
|
||
this.inputs.getToolPreviewLimits(),
|
||
this.customFormatters,
|
||
);
|
||
|
||
const infraDirs = this.inputs.getInfrastructureReadDirs();
|
||
|
||
const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
|
||
() =>
|
||
describeSkillReadGate(tcc, normalizer, () =>
|
||
this.inputs.getActiveSkillEntries(),
|
||
),
|
||
() =>
|
||
describePathGate(tcc, this.resolver, normalizer, this.customExtractors),
|
||
() =>
|
||
describeExternalDirectoryGate(
|
||
tcc,
|
||
infraDirs,
|
||
this.resolver,
|
||
normalizer,
|
||
this.customExtractors,
|
||
),
|
||
() => describeBashExternalDirectoryGate(tcc, bashProgram, this.resolver),
|
||
() => describeBashPathGate(tcc, bashProgram, this.resolver),
|
||
() => {
|
||
const { toolCheck, accessPath } = this.resolvePerToolCheck(
|
||
tcc,
|
||
shell,
|
||
bashProgram,
|
||
normalizer,
|
||
);
|
||
const toolDescriptor = describeToolGate(
|
||
tcc,
|
||
toolCheck,
|
||
formatter,
|
||
accessPath,
|
||
shell,
|
||
);
|
||
toolDescriptor.preCheck = toolCheck;
|
||
return toolDescriptor;
|
||
},
|
||
];
|
||
|
||
for (const produce of gateProducers) {
|
||
const outcome = await runner.run(await produce(), tcc.agentName);
|
||
if (outcome.action === "block") {
|
||
return outcome;
|
||
}
|
||
}
|
||
|
||
return { action: "allow" };
|
||
}
|
||
|
||
/**
|
||
* Resolve the per-tool gate's check, choosing the intent by tool shape:
|
||
* bash chains its sub-commands; a path-bearing tool with a path emits an
|
||
* `access-path` intent (so the per-tool surface matches lexical ∪ canonical,
|
||
* #502); every other tool (and a path-bearing tool with no path) keeps the
|
||
* raw `tool` intent the manager normalizes.
|
||
*
|
||
* Returns the `AccessPath` alongside the check so `describeToolGate` derives
|
||
* the session-approval value from `accessPath.value()`.
|
||
*/
|
||
private resolvePerToolCheck(
|
||
tcc: ToolCallContext,
|
||
shell: ShellInvocation | null,
|
||
bashProgram: BashProgram | null,
|
||
normalizer: PathNormalizer,
|
||
): { toolCheck: PermissionCheckResult; accessPath?: AccessPath } {
|
||
if (shell) {
|
||
if (bashProgram) {
|
||
const check = resolveBashCommandCheck(
|
||
bashProgram.commandText(),
|
||
bashProgram.commands(),
|
||
tcc.agentName ?? undefined,
|
||
this.resolver,
|
||
);
|
||
return {
|
||
toolCheck: applyShellDecisionFloor(check, shell.decisionFloor),
|
||
};
|
||
}
|
||
// A shell invocation whose command did not parse (e.g. empty) still
|
||
// resolves on the `bash` surface, so an aliased tool never falls through
|
||
// to its own extension-tool surface.
|
||
const check = this.resolver.resolve({
|
||
kind: "tool",
|
||
surface: "bash",
|
||
input: { command: shell.command },
|
||
agentName: tcc.agentName ?? undefined,
|
||
});
|
||
return {
|
||
toolCheck: applyShellDecisionFloor(check, shell.decisionFloor),
|
||
};
|
||
}
|
||
|
||
const filePath = getPathBearingToolPath(tcc.toolName, tcc.input);
|
||
if (filePath !== null) {
|
||
const accessPath = normalizer.forPath(filePath);
|
||
return {
|
||
accessPath,
|
||
toolCheck: this.resolver.resolve({
|
||
kind: "access-path",
|
||
surface: tcc.toolName,
|
||
path: accessPath,
|
||
agentName: tcc.agentName ?? undefined,
|
||
}),
|
||
};
|
||
}
|
||
|
||
return {
|
||
toolCheck: this.resolver.resolve({
|
||
kind: "tool",
|
||
surface: tcc.toolName,
|
||
input: tcc.input,
|
||
agentName: tcc.agentName ?? undefined,
|
||
}),
|
||
};
|
||
}
|
||
}
|