mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Merge branch 'main' into vincentkoc-code/config-log-spam-dedupe
This commit is contained in:
47
.github/actions/ensure-base-commit/action.yml
vendored
Normal file
47
.github/actions/ensure-base-commit/action.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Ensure base commit
|
||||
description: Ensure a shallow checkout has enough history to diff against a base SHA.
|
||||
inputs:
|
||||
base-sha:
|
||||
description: Base commit SHA to diff against.
|
||||
required: true
|
||||
fetch-ref:
|
||||
description: Branch or ref to deepen/fetch from origin when base-sha is missing.
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Ensure base commit is available
|
||||
shell: bash
|
||||
env:
|
||||
BASE_SHA: ${{ inputs.base-sha }}
|
||||
FETCH_REF: ${{ inputs.fetch-ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$BASE_SHA" ] || [[ "$BASE_SHA" =~ ^0+$ ]]; then
|
||||
echo "No concrete base SHA available; skipping targeted fetch."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
|
||||
echo "Base commit already present: $BASE_SHA"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for deepen_by in 25 100 300; do
|
||||
echo "Base commit missing; deepening $FETCH_REF by $deepen_by."
|
||||
git fetch --no-tags --deepen="$deepen_by" origin "$FETCH_REF" || true
|
||||
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
|
||||
echo "Resolved base commit after deepening: $BASE_SHA"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Base commit still missing; fetching full history for $FETCH_REF."
|
||||
git fetch --no-tags origin "$FETCH_REF" || true
|
||||
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
|
||||
echo "Resolved base commit after full ref fetch: $BASE_SHA"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Base commit still unavailable after fetch attempts: $BASE_SHA"
|
||||
57
.github/workflows/ci.yml
vendored
57
.github/workflows/ci.yml
vendored
@@ -21,10 +21,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
submodules: false
|
||||
|
||||
- name: Ensure docs-scope base commit
|
||||
uses: ./.github/actions/ensure-base-commit
|
||||
with:
|
||||
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Detect docs-only changes
|
||||
id: check
|
||||
uses: ./.github/actions/detect-docs-changes
|
||||
@@ -46,10 +52,16 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
submodules: false
|
||||
|
||||
- name: Ensure changed-scope base commit
|
||||
uses: ./.github/actions/ensure-base-commit
|
||||
with:
|
||||
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Detect changed scopes
|
||||
id: scope
|
||||
shell: bash
|
||||
@@ -75,6 +87,13 @@ jobs:
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Ensure secrets base commit (PR fast path)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: ./.github/actions/ensure-base-commit
|
||||
with:
|
||||
base-sha: ${{ github.event.pull_request.base.sha }}
|
||||
fetch-ref: ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
@@ -303,13 +322,39 @@ jobs:
|
||||
- name: Install pre-commit
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pre-commit detect-secrets==1.5.0
|
||||
python -m pip install pre-commit
|
||||
|
||||
- name: Detect secrets
|
||||
run: |
|
||||
if ! detect-secrets scan --baseline .secrets.baseline; then
|
||||
echo "::error::Secret scanning failed. See docs/gateway/security.md#secret-scanning-detect-secrets"
|
||||
exit 1
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
echo "Skipping detect-secrets on main until the allowlist cleanup lands."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
echo "Running full detect-secrets scan on push."
|
||||
pre-commit run --all-files detect-secrets
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
changed_files=()
|
||||
if git rev-parse --verify "$BASE^{commit}" >/dev/null 2>&1; then
|
||||
while IFS= read -r path; do
|
||||
[ -n "$path" ] || continue
|
||||
[ -f "$path" ] || continue
|
||||
changed_files+=("$path")
|
||||
done < <(git diff --name-only --diff-filter=ACMR "$BASE" HEAD)
|
||||
fi
|
||||
|
||||
if [ "${#changed_files[@]}" -gt 0 ]; then
|
||||
echo "Running detect-secrets on ${#changed_files[@]} changed file(s)."
|
||||
pre-commit run detect-secrets --files "${changed_files[@]}"
|
||||
else
|
||||
echo "Falling back to full detect-secrets scan."
|
||||
pre-commit run --all-files detect-secrets
|
||||
fi
|
||||
|
||||
- name: Detect committed private keys
|
||||
|
||||
8
.github/workflows/install-smoke.yml
vendored
8
.github/workflows/install-smoke.yml
vendored
@@ -19,9 +19,15 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
|
||||
- name: Ensure docs-scope base commit
|
||||
uses: ./.github/actions/ensure-base-commit
|
||||
with:
|
||||
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
|
||||
fetch-ref: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Detect docs-only changes
|
||||
id: check
|
||||
uses: ./.github/actions/detect-docs-changes
|
||||
|
||||
@@ -234,6 +234,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugin runtime/events: expose `runtime.events.onAgentEvent` and `runtime.events.onSessionTranscriptUpdate` for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic.
|
||||
- CLI/Banner taglines: add `cli.banner.taglineMode` (`random` | `default` | `off`) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior.
|
||||
- Agents/compaction safeguard quality-audit rollout: keep summary quality audits disabled by default unless `agents.defaults.compaction.qualityGuard` is explicitly enabled, and add config plumbing for bounded retry control. (#25556) thanks @rodrigouroz.
|
||||
- Gateway/input_image MIME validation: sniff uploaded image bytes before MIME allowlist enforcement again so declared image types cannot mask concrete non-image payloads, while keeping HEIC/HEIF normalization behavior scoped to actual HEIC inputs. Thanks @vincentkoc.
|
||||
|
||||
### Breaking
|
||||
|
||||
|
||||
@@ -1158,19 +1158,22 @@ If your AI does something bad:
|
||||
|
||||
## Secret Scanning (detect-secrets)
|
||||
|
||||
CI runs `detect-secrets scan --baseline .secrets.baseline` in the `secrets` job.
|
||||
If it fails, there are new candidates not yet in the baseline.
|
||||
CI runs the `detect-secrets` pre-commit hook in the `secrets` job.
|
||||
Pushes to `main` always run an all-files scan. Pull requests use a changed-file
|
||||
fast path when a base commit is available, and fall back to an all-files scan
|
||||
otherwise. If it fails, there are new candidates not yet in the baseline.
|
||||
|
||||
### If CI fails
|
||||
|
||||
1. Reproduce locally:
|
||||
|
||||
```bash
|
||||
detect-secrets scan --baseline .secrets.baseline
|
||||
pre-commit run --all-files detect-secrets
|
||||
```
|
||||
|
||||
2. Understand the tools:
|
||||
- `detect-secrets scan` finds candidates and compares them to the baseline.
|
||||
- `detect-secrets` in pre-commit runs `detect-secrets-hook` with the repo's
|
||||
baseline and excludes.
|
||||
- `detect-secrets audit` opens an interactive review to mark each baseline
|
||||
item as real or false positive.
|
||||
3. For real secrets: rotate/remove them, then re-run the scan to update the baseline.
|
||||
|
||||
@@ -99,7 +99,9 @@ describe("HEIC input image normalization", () => {
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps declared MIME for non-HEIC images without sniffing", async () => {
|
||||
it("keeps declared MIME for non-HEIC images after validation", async () => {
|
||||
detectMimeMock.mockResolvedValueOnce("image/png");
|
||||
|
||||
const image = await extractImageContentFromSource(
|
||||
{
|
||||
type: "base64",
|
||||
@@ -115,7 +117,7 @@ describe("HEIC input image normalization", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(detectMimeMock).not.toHaveBeenCalled();
|
||||
expect(detectMimeMock).toHaveBeenCalledTimes(1);
|
||||
expect(convertHeicToJpegMock).not.toHaveBeenCalled();
|
||||
expect(image).toEqual({
|
||||
type: "image",
|
||||
@@ -123,6 +125,59 @@ describe("HEIC input image normalization", () => {
|
||||
mimeType: "image/png",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects spoofed base64 images when detected bytes are not an image", async () => {
|
||||
detectMimeMock.mockResolvedValueOnce("application/pdf");
|
||||
|
||||
await expect(
|
||||
extractImageContentFromSource(
|
||||
{
|
||||
type: "base64",
|
||||
data: Buffer.from("%PDF-1.4\n").toString("base64"),
|
||||
mediaType: "image/png",
|
||||
},
|
||||
{
|
||||
allowUrl: false,
|
||||
allowedMimes: new Set(["image/png", "image/jpeg"]),
|
||||
maxBytes: 1024 * 1024,
|
||||
maxRedirects: 0,
|
||||
timeoutMs: 1,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("Unsupported image MIME type: application/pdf");
|
||||
expect(convertHeicToJpegMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects spoofed URL images when detected bytes are not an image", async () => {
|
||||
const release = vi.fn(async () => {});
|
||||
fetchWithSsrFGuardMock.mockResolvedValueOnce({
|
||||
response: new Response(Buffer.from("%PDF-1.4\n"), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
release,
|
||||
finalUrl: "https://example.com/photo.png",
|
||||
});
|
||||
detectMimeMock.mockResolvedValueOnce("application/pdf");
|
||||
|
||||
await expect(
|
||||
extractImageContentFromSource(
|
||||
{
|
||||
type: "url",
|
||||
url: "https://example.com/photo.png",
|
||||
},
|
||||
{
|
||||
allowUrl: true,
|
||||
allowedMimes: new Set(["image/png", "image/jpeg"]),
|
||||
maxBytes: 1024 * 1024,
|
||||
maxRedirects: 0,
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("Unsupported image MIME type: application/pdf");
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
expect(convertHeicToJpegMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchWithGuard", () => {
|
||||
|
||||
@@ -235,11 +235,17 @@ async function normalizeInputImage(params: {
|
||||
limits: InputImageLimits;
|
||||
}): Promise<InputImageContent> {
|
||||
const declaredMime = normalizeMimeType(params.mimeType) ?? "application/octet-stream";
|
||||
const sourceMime = HEIC_INPUT_IMAGE_MIMES.has(declaredMime)
|
||||
? (normalizeMimeType(
|
||||
await detectMime({ buffer: params.buffer, headerMime: params.mimeType }),
|
||||
) ?? declaredMime)
|
||||
: declaredMime;
|
||||
const detectedMime = normalizeMimeType(
|
||||
await detectMime({ buffer: params.buffer, headerMime: params.mimeType }),
|
||||
);
|
||||
if (declaredMime.startsWith("image/") && detectedMime && !detectedMime.startsWith("image/")) {
|
||||
throw new Error(`Unsupported image MIME type: ${detectedMime}`);
|
||||
}
|
||||
const sourceMime =
|
||||
(detectedMime && HEIC_INPUT_IMAGE_MIMES.has(detectedMime)) ||
|
||||
(HEIC_INPUT_IMAGE_MIMES.has(declaredMime) && !detectedMime)
|
||||
? (detectedMime ?? declaredMime)
|
||||
: declaredMime;
|
||||
if (!params.limits.allowedMimes.has(sourceMime)) {
|
||||
throw new Error(`Unsupported image MIME type: ${sourceMime}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user