mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix: validate frozen Telegram explicit command denials (#150761)
This commit is contained in:
parent
847aef50ad
commit
35abd04c0b
4 changed files with 195 additions and 2 deletions
1
.github/workflows/npm-telegram-beta-e2e.yml
vendored
1
.github/workflows/npm-telegram-beta-e2e.yml
vendored
|
|
@ -348,6 +348,7 @@ jobs:
|
|||
qa/scenarios/channels/telegram-progress-tool-visibility.yaml
|
||||
qa/scenarios/channels/telegram-queue-invalid-mode.yaml
|
||||
qa/scenarios/channels/telegram-rich-inline-composition.yaml
|
||||
qa/scenarios/channels/telegram-repeated-command-authorization.yaml
|
||||
src/agents/embedded-agent-subscribe.ts
|
||||
|
||||
- name: Resolve frozen package Telegram scenarios
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { execFileSync } from "node:child_process";
|
||||
import { appendFileSync, readFileSync } from "node:fs";
|
||||
import { appendFileSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const PARTIAL_FAILURE_RECOVERY_SCENARIO = "telegram-partial-failure-recovery";
|
||||
const SETTLED_EMPTY_RESPONSE_SCENARIO = "telegram-empty-response-after-write-recovery";
|
||||
|
|
@ -13,6 +13,37 @@ const POLICY_HOT_RELOAD_SCENARIOS = [
|
|||
"telegram-policy-hot-reload",
|
||||
"telegram-group-policy-hot-reload",
|
||||
];
|
||||
const REPEATED_COMMAND_SCENARIO =
|
||||
"qa/scenarios/channels/telegram-repeated-command-authorization.yaml";
|
||||
const SILENT_DENIAL = " - waitForNoOutbound:\n quietMs: 3000\n";
|
||||
const EXPLICIT_DENIAL =
|
||||
" - waitForOutbound:\n" +
|
||||
" conversation: { id: telegram-command-room, kind: channel }\n" +
|
||||
" textIncludes: You are not authorized to use this command.\n" +
|
||||
" timeoutMs: 60000\n";
|
||||
|
||||
function resolveFrozenTelegramCommandScenario(sourceRoot: string): string | undefined {
|
||||
const target = readSource(sourceRoot, REPEATED_COMMAND_SCENARIO);
|
||||
if (target === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const hasExplicitDenial = target.split(EXPLICIT_DENIAL).length === 2;
|
||||
const hasSilentDenial = target.split(SILENT_DENIAL).length === 2;
|
||||
if (hasExplicitDenial === hasSilentDenial) {
|
||||
throw new Error("unrecognized frozen Telegram repeated-command denial contract");
|
||||
}
|
||||
if (!hasExplicitDenial) {
|
||||
return undefined;
|
||||
}
|
||||
// Target source selects a known contract; only trusted tooling supplies executable YAML.
|
||||
// Retire this projection when no supported frozen release returns explicit denials.
|
||||
const trustedRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
||||
const trusted = readFileSync(path.join(trustedRoot, REPEATED_COMMAND_SCENARIO), "utf8");
|
||||
if (trusted.split(SILENT_DENIAL).length !== 2 || trusted.includes(EXPLICIT_DENIAL)) {
|
||||
throw new Error("trusted Telegram repeated-command denial action changed");
|
||||
}
|
||||
return trusted.replace(SILENT_DENIAL, EXPLICIT_DENIAL);
|
||||
}
|
||||
|
||||
function readSource(sourceRoot: string, relativePath: string): string | undefined {
|
||||
try {
|
||||
|
|
@ -100,6 +131,21 @@ function main(): void {
|
|||
if (actualSha !== selectedSha) {
|
||||
throw new Error("frozen Telegram source checkout does not match package source SHA");
|
||||
}
|
||||
const commandScenario = resolveFrozenTelegramCommandScenario(sourceRoot);
|
||||
if (commandScenario !== undefined) {
|
||||
const overlayDir = mkdtempSync(
|
||||
path.join(path.dirname(path.resolve(sourceRoot)), "telegram-contract-"),
|
||||
);
|
||||
const overlay = path.join(overlayDir, path.basename(REPEATED_COMMAND_SCENARIO));
|
||||
writeFileSync(
|
||||
overlay,
|
||||
`# Frozen package source ${actualSha}: explicit authorization denial.\n${commandScenario}`,
|
||||
);
|
||||
appendFileSync(output, `OPENCLAW_NPM_TELEGRAM_COMMAND_SCENARIO=${overlay}\n`);
|
||||
process.stdout.write(
|
||||
`Telegram repeated-command authorization: explicit denial contract from ${actualSha}\n`,
|
||||
);
|
||||
}
|
||||
const omittedScenarios = resolveFrozenTelegramScenarioOmissions(sourceRoot);
|
||||
if (omittedScenarios.length > 0) {
|
||||
appendFileSync(
|
||||
|
|
|
|||
|
|
@ -429,6 +429,14 @@ EOF
|
|||
|
||||
# Mount the trusted current-source QA harness separately from the installed
|
||||
# package candidate. The candidate remains the absolute CLI/runtime SUT.
|
||||
command_scenario_mount_args=()
|
||||
if [ -n "${OPENCLAW_NPM_TELEGRAM_COMMAND_SCENARIO:-}" ]; then
|
||||
if [ ! -f "$OPENCLAW_NPM_TELEGRAM_COMMAND_SCENARIO" ]; then
|
||||
echo "Frozen Telegram command scenario is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
command_scenario_mount_args=(-v "$OPENCLAW_NPM_TELEGRAM_COMMAND_SCENARIO:/app/qa/scenarios/channels/telegram-repeated-command-authorization.yaml:ro")
|
||||
fi
|
||||
run_logged_print_heartbeat "npm-telegram-live-suite" 60 docker_e2e_run_with_harness \
|
||||
"${docker_env[@]}" \
|
||||
-v "$ROOT_DIR/.artifacts:/app/.artifacts" \
|
||||
|
|
@ -442,6 +450,7 @@ run_logged_print_heartbeat "npm-telegram-live-suite" 60 docker_e2e_run_with_harn
|
|||
-v "$ROOT_DIR/.agents:/app/.agents:ro" \
|
||||
-v "$ROOT_DIR/taxonomy.yaml:/app/taxonomy.yaml:ro" \
|
||||
-v "$ROOT_DIR/qa/scenarios:/app/qa/scenarios:ro" \
|
||||
${command_scenario_mount_args[@]+"${command_scenario_mount_args[@]}"} \
|
||||
${prepublish_registry_mount_args[@]+"${prepublish_registry_mount_args[@]}"} \
|
||||
-v "$npm_prefix_host:/npm-global" \
|
||||
-i "$IMAGE_NAME" bash -s <<'EOF'
|
||||
|
|
|
|||
137
test/scripts/npm-telegram-frozen-command.test.ts
Normal file
137
test/scripts/npm-telegram-frozen-command.test.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import YAML from "yaml";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
|
||||
const scenarioPath = "qa/scenarios/channels/telegram-repeated-command-authorization.yaml";
|
||||
const trusted = readFileSync(path.join(repoRoot, scenarioPath), "utf8");
|
||||
const silent = " - waitForNoOutbound:\n quietMs: 3000\n";
|
||||
const explicit =
|
||||
" - waitForOutbound:\n" +
|
||||
" conversation: { id: telegram-command-room, kind: channel }\n" +
|
||||
" textIncludes: You are not authorized to use this command.\n" +
|
||||
" timeoutMs: 60000\n";
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
function fixture(source: string) {
|
||||
const root = tempDirs.make("telegram-frozen-command-");
|
||||
const target = path.join(root, "target");
|
||||
const file = path.join(target, scenarioPath);
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
writeFileSync(file, source);
|
||||
execFileSync("git", ["init", "-q", target]);
|
||||
execFileSync("git", ["-C", target, "add", "."]);
|
||||
execFileSync("git", [
|
||||
"-C",
|
||||
target,
|
||||
"-c",
|
||||
"user.name=Fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.test",
|
||||
"commit",
|
||||
"-qm",
|
||||
"target",
|
||||
]);
|
||||
const sha = execFileSync("git", ["-C", target, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
||||
const output = path.join(root, "github.env");
|
||||
return {
|
||||
root,
|
||||
sha,
|
||||
resolve: (selectedSha = sha) => {
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(repoRoot, "scripts/e2e/lib/npm-telegram-live/resolve-target-scenarios.mts"),
|
||||
target,
|
||||
],
|
||||
{
|
||||
env: { ...process.env, OPENCLAW_SELECTED_SHA: selectedSha, GITHUB_ENV: output },
|
||||
stdio: "pipe",
|
||||
},
|
||||
);
|
||||
return Object.fromEntries(
|
||||
readFileSync(output, "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => {
|
||||
const equal = line.indexOf("=");
|
||||
return [line.slice(0, equal), line.slice(equal + 1)];
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function captureSuiteMounts(overlay?: string) {
|
||||
const script = readFileSync(
|
||||
path.join(repoRoot, "scripts/e2e/npm-telegram-live-docker.sh"),
|
||||
"utf8",
|
||||
);
|
||||
const mountCall = script.slice(
|
||||
script.indexOf("command_scenario_mount_args=()"),
|
||||
script.indexOf(" bash -s <<'EOF'", script.indexOf("command_scenario_mount_args=()")),
|
||||
);
|
||||
return execFileSync(
|
||||
"/bin/bash",
|
||||
[
|
||||
"-eu",
|
||||
"-c",
|
||||
`
|
||||
docker_env=(-e FIXTURE=1)
|
||||
prepublish_registry_mount_args=()
|
||||
ROOT_DIR=/trusted
|
||||
OUTPUT_DIR_HOST=/output
|
||||
OUTPUT_DIR_CONTAINER=/app/output
|
||||
harness_package_json=/trusted/package.json
|
||||
npm_prefix_host=/npm
|
||||
IMAGE_NAME=fixture
|
||||
run_logged_print_heartbeat() { printf '%s\\n' "$@"; }
|
||||
${mountCall}
|
||||
`,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, OPENCLAW_NPM_TELEGRAM_COMMAND_SCENARIO: overlay ?? "" },
|
||||
},
|
||||
)
|
||||
.trim()
|
||||
.split("\n");
|
||||
}
|
||||
|
||||
it("mounts a source-qualified trusted denial action without changing restored command proof", () => {
|
||||
// Frozen YAML selects the contract but must never supply executable expressions.
|
||||
const f = fixture(
|
||||
trusted.replace(silent, explicit).replace("title:", "# target-only-untrusted-content\ntitle:"),
|
||||
);
|
||||
const env = f.resolve();
|
||||
const overlay = env.OPENCLAW_NPM_TELEGRAM_COMMAND_SCENARIO;
|
||||
assert.ok(overlay);
|
||||
const actual = readFileSync(overlay, "utf8");
|
||||
expect(actual).toContain(`# Frozen package source ${f.sha}: explicit authorization denial.`);
|
||||
expect(actual).not.toContain("target-only-untrusted-content");
|
||||
expect(YAML.parse(actual)).toEqual(YAML.parse(trusted.replace(silent, explicit)));
|
||||
expect(env.OPENCLAW_NPM_TELEGRAM_OMIT_DEFAULT_SCENARIOS).not.toContain(
|
||||
"telegram-repeated-command-authorization",
|
||||
);
|
||||
const mounts = captureSuiteMounts(overlay);
|
||||
const baseIndex = mounts.indexOf("/trusted/qa/scenarios:/app/qa/scenarios:ro");
|
||||
expect(baseIndex).toBeGreaterThan(0);
|
||||
expect(mounts.indexOf(`${overlay}:/app/${scenarioPath}:ro`)).toBeGreaterThan(baseIndex);
|
||||
});
|
||||
|
||||
it("keeps current silent denial on the normal trusted scenario mount", () => {
|
||||
const env = fixture(trusted).resolve();
|
||||
expect(env.OPENCLAW_NPM_TELEGRAM_COMMAND_SCENARIO).toBeUndefined();
|
||||
expect(captureSuiteMounts().filter((arg) => arg.includes(scenarioPath))).toEqual([]);
|
||||
});
|
||||
|
||||
it("refuses unknown source contracts and mismatched selected source identities", () => {
|
||||
expect(() => fixture(trusted.replace(silent, "")).resolve()).toThrow();
|
||||
expect(() => fixture(trusted.replace(silent, explicit)).resolve("0".repeat(40))).toThrow();
|
||||
expect(() => captureSuiteMounts("/missing/frozen-scenario.yaml")).toThrow();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue