mirror of
https://github.com/openclaw/lobster.git
synced 2026-08-14 00:48:09 +00:00
fix: harden lobster release and invoke paths
This commit is contained in:
@@ -77,6 +77,12 @@ jobs:
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
case "$job" in
|
||||
''|*[!A-Za-z0-9._-]*)
|
||||
echo "Invalid crabbox_job" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
mkdir -p "$HOME/.crabbox/actions"
|
||||
state="$HOME/.crabbox/actions/${CRABBOX_ID}.env"
|
||||
env_file="$HOME/.crabbox/actions/${CRABBOX_ID}.env.sh"
|
||||
|
||||
@@ -517,6 +517,33 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! VERSION_A="${latest_version}" VERSION_B="${RELEASE_VERSION}" node <<'NODE'
|
||||
function parseVersion(value) {
|
||||
const match = /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9]+))?$/.exec(value);
|
||||
if (!match) throw new Error(`Invalid version: ${value}`);
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
prerelease: match[4] === undefined ? null : Number(match[4]),
|
||||
};
|
||||
}
|
||||
const latest = parseVersion(process.env.VERSION_A);
|
||||
const release = parseVersion(process.env.VERSION_B);
|
||||
const fields = ["major", "minor", "patch"];
|
||||
for (const field of fields) {
|
||||
if (latest[field] > release[field]) process.exit(1);
|
||||
if (latest[field] < release[field]) process.exit(0);
|
||||
}
|
||||
if (latest.prerelease === null && release.prerelease !== null) process.exit(1);
|
||||
if (latest.prerelease !== null && release.prerelease === null) process.exit(0);
|
||||
if ((latest.prerelease ?? 0) > (release.prerelease ?? 0)) process.exit(1);
|
||||
NODE
|
||||
then
|
||||
echo "npm latest ${latest_version} is newer than release ${RELEASE_VERSION}; refusing downgrade promotion." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! npm view "${PACKAGE_NAME}@${RELEASE_VERSION}" version >/dev/null 2>&1; then
|
||||
echo "${PACKAGE_NAME}@${RELEASE_VERSION} is not published on npm." >&2
|
||||
exit 1
|
||||
|
||||
+13
-7
@@ -1,18 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
function shellQuote(arg) {
|
||||
if (/^[A-Za-z0-9_\-./:=@]+$/.test(arg)) return arg;
|
||||
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
||||
if (/^[A-Za-z0-9_\-./:=@]+$/.test(arg)) return arg;
|
||||
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const pipeline = ['clawd.invoke', ...argv.map(shellQuote)].join(' ');
|
||||
const pipeline = ["clawd.invoke", ...argv.map(shellQuote)].join(" ");
|
||||
const lobsterBin = join(dirname(fileURLToPath(import.meta.url)), "lobster.js");
|
||||
|
||||
const res = spawnSync('lobster', [pipeline], {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
const res = spawnSync(process.execPath, [lobsterBin, pipeline], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
if (res.error) {
|
||||
console.error(`clawd.invoke failed to spawn lobster: ${res.error.message}`);
|
||||
}
|
||||
|
||||
process.exit(res.status ?? 1);
|
||||
|
||||
+7
-7
@@ -8,17 +8,17 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
async function load() {
|
||||
const distEntry = join(__dirname, "../dist/src/cli.js");
|
||||
if (existsSync(distEntry)) {
|
||||
return import(pathToFileURL(distEntry).href);
|
||||
}
|
||||
const srcEntry = join(__dirname, "../src/cli.js");
|
||||
return import(pathToFileURL(srcEntry).href);
|
||||
const distEntry = join(__dirname, "../dist/src/cli.js");
|
||||
if (existsSync(distEntry)) {
|
||||
return import(pathToFileURL(distEntry).href);
|
||||
}
|
||||
const srcEntry = join(__dirname, "../src/cli.js");
|
||||
return import(pathToFileURL(srcEntry).href);
|
||||
}
|
||||
|
||||
const mod = await load();
|
||||
if (typeof mod.runCli !== "function") {
|
||||
throw new Error("lobster CLI entrypoint missing runCli()");
|
||||
throw new Error("lobster CLI entrypoint missing runCli()");
|
||||
}
|
||||
|
||||
await mod.runCli(process.argv.slice(2));
|
||||
|
||||
+16
-10
@@ -1,21 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
function shellQuote(arg) {
|
||||
// Conservative POSIX-ish quoting for embedding argv into a single pipeline string.
|
||||
// Lobster's pipeline parser preserves quoted substrings.
|
||||
if (/^[A-Za-z0-9_\-./:=@]+$/.test(arg)) return arg;
|
||||
// single-quote, escaping embedded single quotes: ' -> '\''
|
||||
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
||||
// Conservative POSIX-ish quoting for embedding argv into a single pipeline string.
|
||||
// Lobster's pipeline parser preserves quoted substrings.
|
||||
if (/^[A-Za-z0-9_\-./:=@]+$/.test(arg)) return arg;
|
||||
// single-quote, escaping embedded single quotes: ' -> '\''
|
||||
return `'${String(arg).replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const pipeline = ['openclaw.invoke', ...argv.map(shellQuote)].join(' ');
|
||||
const pipeline = ["openclaw.invoke", ...argv.map(shellQuote)].join(" ");
|
||||
const lobsterBin = join(dirname(fileURLToPath(import.meta.url)), "lobster.js");
|
||||
|
||||
const res = spawnSync('lobster', [pipeline], {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
const res = spawnSync(process.execPath, [lobsterBin, pipeline], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
if (res.error) {
|
||||
console.error(`openclaw.invoke failed to spawn lobster: ${res.error.message}`);
|
||||
}
|
||||
|
||||
process.exit(res.status ?? 1);
|
||||
|
||||
+5
-5
@@ -42,14 +42,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||
"build": "pnpm clean && tsgo -p tsconfig.json",
|
||||
"build": "pnpm clean && tsgo -p tsconfig.build.json",
|
||||
"prepack": "pnpm build",
|
||||
"typecheck": "tsgo -p tsconfig.json --noEmit",
|
||||
"format": "oxfmt --write package.json tsconfig.json src test",
|
||||
"format:check": "oxfmt --check package.json tsconfig.json src test",
|
||||
"lint": "pnpm format:check && oxlint --tsconfig tsconfig.json src test",
|
||||
"format": "oxfmt --write package.json tsconfig.json tsconfig.build.json bin src test",
|
||||
"format:check": "oxfmt --check package.json tsconfig.json tsconfig.build.json bin src test",
|
||||
"lint": "pnpm format:check && oxlint --tsconfig tsconfig.json bin src test",
|
||||
"fmt": "pnpm format",
|
||||
"test": "pnpm build && node --test dist/test/*.test.js",
|
||||
"test": "pnpm clean && tsgo -p tsconfig.json && node -e \"if (!require('fs').existsSync('dist/test')) process.exit(1)\" && node --test dist/test/*.test.js",
|
||||
"check:changed": "pnpm run test",
|
||||
"test:changed": "pnpm run test",
|
||||
"crabbox:hydrate": "crabbox actions hydrate",
|
||||
|
||||
@@ -58,8 +58,9 @@ function createInvokeCommand(commandName: string) {
|
||||
const action = args.action;
|
||||
if (!tool || !action) throw new Error(`${commandName} requires --tool and --action`);
|
||||
|
||||
const explicitToken = args.token !== undefined && args.token !== null;
|
||||
const token = String(
|
||||
args.token ?? ctx.env.OPENCLAW_TOKEN ?? ctx.env.CLAWD_TOKEN ?? "",
|
||||
explicitToken ? args.token : ctx.env.OPENCLAW_TOKEN ?? ctx.env.CLAWD_TOKEN ?? "",
|
||||
).trim();
|
||||
|
||||
let toolArgs: any = {};
|
||||
@@ -76,6 +77,14 @@ function createInvokeCommand(commandName: string) {
|
||||
}
|
||||
|
||||
const endpoint = new URL("/tools/invoke", url);
|
||||
if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:") {
|
||||
throw new Error(`${commandName} requires an http(s) --url`);
|
||||
}
|
||||
if (token && !explicitToken && !isLocalOpenClawOrigin(endpoint)) {
|
||||
throw new Error(
|
||||
`${commandName} refuses to send OPENCLAW_TOKEN/CLAWD_TOKEN to non-local --url; pass --token explicitly for remote endpoints`,
|
||||
);
|
||||
}
|
||||
const sessionKey = args.sessionKey ?? args["session-key"] ?? null;
|
||||
const dryRun = args.dryRun ?? args["dry-run"] ?? null;
|
||||
|
||||
@@ -146,5 +155,10 @@ async function* asStream(items: any[]) {
|
||||
for (const item of items) yield item;
|
||||
}
|
||||
|
||||
function isLocalOpenClawOrigin(url: URL): boolean {
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
||||
}
|
||||
|
||||
export const openclawInvokeCommand = createInvokeCommand("openclaw.invoke");
|
||||
export const clawdInvokeCommand = createInvokeCommand("clawd.invoke");
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
test("packaged lobster bin starts and prints help", () => {
|
||||
const bin = path.join(process.cwd(), "bin", "lobster.js");
|
||||
const res = spawnSync(process.execPath, [bin, "--help"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /Usage:/);
|
||||
});
|
||||
@@ -129,3 +129,30 @@ test("openclaw.invoke --each maps input items into tool args", async () => {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("openclaw.invoke refuses env tokens for non-local URLs", async () => {
|
||||
const registry = createDefaultRegistry();
|
||||
const cmd = registry.get("openclaw.invoke");
|
||||
|
||||
await assert.rejects(
|
||||
cmd.run({
|
||||
input: streamOf([]),
|
||||
args: {
|
||||
_: [],
|
||||
url: "https://example.com",
|
||||
tool: "demo",
|
||||
action: "ping",
|
||||
},
|
||||
ctx: {
|
||||
stdin: process.stdin,
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
env: { ...process.env, OPENCLAW_TOKEN: "secret" },
|
||||
registry,
|
||||
mode: "tool",
|
||||
render: { json() {}, lines() {} },
|
||||
},
|
||||
}),
|
||||
/refuses to send OPENCLAW_TOKEN\/CLAWD_TOKEN to non-local --url/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules", "test"]
|
||||
}
|
||||
Reference in New Issue
Block a user