mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: add pure ssh2 remote operations
This commit is contained in:
@@ -47,6 +47,8 @@ export interface ShellInvocation {
|
||||
command: string;
|
||||
/** The working directory the command runs in, if the tool projects one. */
|
||||
workdir: string | undefined;
|
||||
/** Optional minimum review decision imposed by a configured shell alias. */
|
||||
decisionFloor?: ShellToolsConfig[string]["decisionFloor"];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +80,7 @@ export function resolveShellInvocation(
|
||||
return {
|
||||
command: getNonEmptyString(record.command) ?? "",
|
||||
workdir: undefined,
|
||||
decisionFloor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,6 +91,7 @@ export function resolveShellInvocation(
|
||||
workdir: alias.workdirArgument
|
||||
? (getNonEmptyString(record[alias.workdirArgument]) ?? undefined)
|
||||
: undefined,
|
||||
decisionFloor: alias.decisionFloor,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -242,17 +242,22 @@ export function mergeUnifiedConfigs(
|
||||
}
|
||||
}
|
||||
|
||||
// shellTools: shallow-merge by tool name so a project entry overrides a
|
||||
// colliding tool's alias but never drops a global entry (a dropped alias is
|
||||
// a silent enforcement regression).
|
||||
// shellTools: merge by tool name, then by descriptor field. A project can
|
||||
// replace argument names, but omitting a global decisionFloor must not silently
|
||||
// remove that security boundary.
|
||||
const baseShell = base.shellTools;
|
||||
const overrideShell = override.shellTools;
|
||||
if (baseShell && overrideShell) {
|
||||
merged.shellTools = { ...baseShell, ...overrideShell };
|
||||
} else if (baseShell) {
|
||||
merged.shellTools = baseShell;
|
||||
} else if (overrideShell) {
|
||||
merged.shellTools = overrideShell;
|
||||
if (baseShell || overrideShell) {
|
||||
const toolNames = new Set([
|
||||
...Object.keys(baseShell ?? {}),
|
||||
...Object.keys(overrideShell ?? {}),
|
||||
]);
|
||||
merged.shellTools = Object.fromEntries(
|
||||
[...toolNames].map((toolName) => [
|
||||
toolName,
|
||||
{ ...baseShell?.[toolName], ...overrideShell?.[toolName] },
|
||||
]),
|
||||
) as ShellToolsConfig;
|
||||
}
|
||||
|
||||
// Permission: deep-shallow merge
|
||||
|
||||
@@ -120,10 +120,16 @@ const shellToolAliasSchema = z
|
||||
description:
|
||||
"Optional name of the tool's input argument holding the working directory (e.g. 'workdir').",
|
||||
}),
|
||||
decisionFloor: z.literal("ask").optional().meta({
|
||||
description:
|
||||
"Require every invocation of this shell alias to be reviewed at least as an ask; deny remains deny.",
|
||||
markdownDescription:
|
||||
"Sets the minimum review decision for this alias. `allow` results from Bash policy are raised to `ask`, while existing `ask` and `deny` results are preserved. Only `\"ask\"` is accepted so a project override cannot weaken a global floor.",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
description:
|
||||
"Maps one shell-aliased tool to the input arguments holding its command and (optionally) its working directory.",
|
||||
"Maps one shell-aliased tool to the input arguments holding its command and optional working directory, plus an optional ask decision floor.",
|
||||
});
|
||||
|
||||
const shellToolsSchema = z
|
||||
@@ -137,10 +143,11 @@ const shellToolsSchema = z
|
||||
description:
|
||||
"Maps non-bash tool names that carry shell semantics to the input arguments holding their command and working directory.",
|
||||
markdownDescription:
|
||||
'Records which non-`bash` tools carry shell semantics, mapping each tool name to the input argument holding its command (and optionally its working directory).\n\nUse this when an extension replaces the native `bash` tool under a different name — e.g. `@howaboua/pi-codex-conversion` registers `exec_command` with a `cmd` argument and an optional `workdir`. Recording the alias lets the permission system gate that tool through the same bash enforcement stack as native `bash` (command decomposition, wrapper flooring, path/external-directory token gates, and `bash:` rules).\n\nExample:\n\n```json\n"shellTools": {\n "exec_command": { "commandArgument": "cmd", "workdirArgument": "workdir" }\n}\n```\n\n**Merge order:** shallow-merge by tool name across global → project. A project entry overrides a specific tool\'s mapping on key collision but never drops a global entry.',
|
||||
'Records which non-`bash` tools carry shell semantics, mapping each tool name to the input argument holding its command (and optionally its working directory).\n\nUse this when an extension replaces the native `bash` tool under a different name — e.g. `@howaboua/pi-codex-conversion` registers `exec_command` with a `cmd` argument and an optional `workdir`. Recording the alias lets the permission system gate that tool through the same bash enforcement stack as native `bash` (command decomposition, wrapper flooring, path/external-directory token gates, and `bash:` rules).\n\nSet `decisionFloor` to `"ask"` when the wrapper crosses an additional trust boundary, such as SSH or container execution. Bash `allow` results are raised to `ask`; existing `ask` and `deny` results remain unchanged.\n\nExample:\n\n```json\n"shellTools": {\n "exec_command": { "commandArgument": "cmd", "workdirArgument": "workdir" },\n "ssh_bash": { "commandArgument": "command", "decisionFloor": "ask" }\n}\n```\n\n**Merge order:** merge each tool mapping by field across global → project. Project values can replace argument names, while a global `decisionFloor: "ask"` survives when the project entry omits it.',
|
||||
examples: [
|
||||
{
|
||||
exec_command: { commandArgument: "cmd", workdirArgument: "workdir" },
|
||||
ssh_bash: { commandArgument: "command", decisionFloor: "ask" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ShellToolsConfig } from "#src/config-schema";
|
||||
import type { PermissionCheckResult } from "#src/types";
|
||||
|
||||
/** Synthetic rule marker recorded when a shell alias raises allow to ask. */
|
||||
export const SHELL_TOOL_DECISION_FLOOR_PATTERN = "<shell-tool-decision-floor>";
|
||||
|
||||
type ShellDecisionFloor = ShellToolsConfig[string]["decisionFloor"];
|
||||
|
||||
/**
|
||||
* Enforce the minimum review decision configured for a shell alias.
|
||||
*
|
||||
* The only supported floor is `ask`: it raises an allow to ask while preserving
|
||||
* existing asks and hard denies. The synthetic result no longer reports a
|
||||
* session source, so a prior Bash session approval cannot bypass an alias whose
|
||||
* contract requires every invocation to enter the authorizer chain.
|
||||
*/
|
||||
export function applyShellDecisionFloor(
|
||||
check: PermissionCheckResult,
|
||||
floor: ShellDecisionFloor,
|
||||
): PermissionCheckResult {
|
||||
if (floor !== "ask" || check.state !== "allow") return check;
|
||||
|
||||
return {
|
||||
...check,
|
||||
state: "ask",
|
||||
source: "bash",
|
||||
matchedPattern: SHELL_TOOL_DECISION_FLOOR_PATTERN,
|
||||
reason: undefined,
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,7 @@ 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";
|
||||
|
||||
/**
|
||||
@@ -164,25 +165,27 @@ export class ToolCallGatePipeline {
|
||||
): { toolCheck: PermissionCheckResult; accessPath?: AccessPath } {
|
||||
if (shell) {
|
||||
if (bashProgram) {
|
||||
const check = resolveBashCommandCheck(
|
||||
bashProgram.commandText(),
|
||||
bashProgram.commands(),
|
||||
tcc.agentName ?? undefined,
|
||||
this.resolver,
|
||||
);
|
||||
return {
|
||||
toolCheck: resolveBashCommandCheck(
|
||||
bashProgram.commandText(),
|
||||
bashProgram.commands(),
|
||||
tcc.agentName ?? undefined,
|
||||
this.resolver,
|
||||
),
|
||||
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: this.resolver.resolve({
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: shell.command },
|
||||
agentName: tcc.agentName ?? undefined,
|
||||
}),
|
||||
toolCheck: applyShellDecisionFloor(check, shell.decisionFloor),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@ export function describeToolGate(
|
||||
tcc.input,
|
||||
PATH_BEARING_TOOLS,
|
||||
);
|
||||
if (shell && tcc.toolName !== check.toolName) {
|
||||
permissionLogContext.toolInputPreview =
|
||||
formatter.formatToolInputForPrompt(tcc.toolName, tcc.input) || undefined;
|
||||
}
|
||||
|
||||
// Compute session approval suggestion for the "for this session" option.
|
||||
const suggestion = suggestSessionPattern(
|
||||
|
||||
@@ -47,7 +47,7 @@ export function buildToolAskPayload(facts: ToolAskFacts): PromptPayload {
|
||||
executedUnit: check.executedUnit ?? null,
|
||||
},
|
||||
evidence: bash
|
||||
? fullCommandEvidence(facts)
|
||||
? [...fullCommandEvidence(facts), ...invokedShellInputPreviewEvidence(facts)]
|
||||
: inputPreviewEvidence(facts, mcp),
|
||||
annotations: [],
|
||||
};
|
||||
@@ -86,6 +86,14 @@ function fullCommandEvidence(facts: ToolAskFacts): PromptEvidence[] {
|
||||
return [{ label: "full command", text: fullCommand, detail: null }];
|
||||
}
|
||||
|
||||
/** Extra context supplied by the concrete tool that exposes shell semantics. */
|
||||
function invokedShellInputPreviewEvidence(facts: ToolAskFacts): PromptEvidence[] {
|
||||
const invokedToolName = distinctInvokedName(facts);
|
||||
if (invokedToolName === null) return [];
|
||||
const preview = facts.formatter?.formatToolInputForPrompt(invokedToolName, facts.input);
|
||||
return preview ? [{ label: "input", text: preview, detail: null }] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The per-tool input preview, when a formatter is registered and produces one.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user