mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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 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) {
|
||||
return {
|
||||
toolCheck: resolveBashCommandCheck(
|
||||
bashProgram.commandText(),
|
||||
bashProgram.commands(),
|
||||
tcc.agentName ?? undefined,
|
||||
this.resolver,
|
||||
),
|
||||
};
|
||||
}
|
||||
// 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.
|
||||
return {
|
||||
toolCheck: this.resolver.resolve({
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: shell.command },
|
||||
agentName: tcc.agentName ?? undefined,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user