Dev to main#3
Conversation
| run: cd web && npm ci | ||
| - name: Start local test env for e2e gates | ||
| if: inputs.gate_stage != 'pr' | ||
| run: make test-env up | ||
|
|
||
| - name: Install gitleaks | ||
| run: | | ||
| set -euo pipefail | ||
| GL_VERSION="8.24.2" | ||
| ARCH="$(uname -m)" | ||
| case "$ARCH" in | ||
| x86_64) GL_ARCH="x64" ;; | ||
| aarch64|arm64) GL_ARCH="arm64" ;; | ||
| *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; | ||
| esac | ||
| curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GL_VERSION}/gitleaks_${GL_VERSION}_linux_${GL_ARCH}.tar.gz" \ | ||
| | tar -xz -C "$RUNNER_TEMP/bin" gitleaks | ||
|
|
||
| - name: Verify security tools | ||
| run: | | ||
| "$RUNNER_TEMP/bin/govulncheck" -version | ||
| "$RUNNER_TEMP/bin/gitleaks" version | ||
| - name: Run gate | ||
| run: make gate "${{ inputs.gate_stage }}" | ||
|
|
||
| - name: Run security checks | ||
| run: make sec fast | ||
| env: | ||
| GOVULNCHECK_BIN: ${{ runner.temp }}/bin/govulncheck | ||
| GITLEAKS_BIN: ${{ runner.temp }}/bin/gitleaks | ||
| GITLEAKS_REPORT_PATH: ${{ env.GITLEAKS_REPORT_PATH }} | ||
| - name: Stop local test env if present | ||
| if: always() && inputs.gate_stage != 'pr' | ||
| run: make test-env down || true | ||
|
|
||
| - name: Upload gitleaks report | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: ${{ env.GITLEAKS_ARTIFACT_NAME }} | ||
| name: gitleaks-report-${{ inputs.gate_stage }}-${{ github.sha }} | ||
| path: ${{ env.GITLEAKS_REPORT_PATH }} | ||
| if-no-files-found: ignore | ||
|
|
||
| - name: Announce gitleaks report | ||
| if: always() | ||
| run: | | ||
| if [ -f "$GITLEAKS_REPORT_PATH" ]; then | ||
| echo "gitleaks report uploaded as artifact: $GITLEAKS_ARTIFACT_NAME" | ||
| echo "Report path: $GITLEAKS_REPORT_PATH" | ||
| { | ||
| echo "### Gitleaks Report" | ||
| echo "Artifact: $GITLEAKS_ARTIFACT_NAME" | ||
| echo "Path: $GITLEAKS_REPORT_PATH" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| else | ||
| echo "No gitleaks report file was generated." | ||
| fi | ||
|
|
||
| e2e: | ||
| name: E2E Smoke | ||
| runs-on: ubuntu-latest | ||
| needs: [lint, test-backend, test-frontend, openapi, sec] | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Prepare e2e artifact directory | ||
| run: mkdir -p build/reports/e2e | ||
|
|
||
| - name: Run container smoke test | ||
| run: make test e2e fast | ||
| env: | ||
| APPOS_E2E_ARTIFACT_DIR: build/reports/e2e | ||
| APPOS_E2E_KEEP_CONTAINER_ON_FAILURE: "1" | ||
|
|
||
| - name: Upload e2e diagnostics | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: e2e-diagnostics-${{ github.sha }} | ||
| name: e2e-diagnostics-${{ inputs.gate_stage }}-${{ github.sha }} | ||
| path: build/reports/e2e | ||
| if-no-files-found: ignore | ||
|
|
||
| - name: Upload release SBOM | ||
| if: always() && inputs.gate_stage == 'release' | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: sbom-${{ github.sha }} | ||
| path: sbom.spdx.json | ||
| if-no-files-found: ignore | ||
|
|
||
| - name: Cleanup preserved e2e containers | ||
| if: always() | ||
| run: docker ps -aq --filter name=appos-e2e | xargs -r docker rm -f | ||
| if: always() && inputs.gate_stage != 'pr' | ||
| run: docker ps -aq --filter name=appos-e2e | xargs -r docker rm -f |
| name: Run Post-Merge Gate | ||
| uses: ./.github/workflows/_quality-gate.yml | ||
| with: | ||
| gate_stage: merge |
| name: Run PR Gate | ||
| uses: ./.github/workflows/_quality-gate.yml | ||
| with: | ||
| gate_stage: pr |
| name: Run Remote Playwright | ||
| timeout-minutes: 60 | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| APPOS_BASE_URL: ${{ secrets.APPOS_BASE_URL }} | ||
| APPOS_SUPERUSER_EMAIL: ${{ secrets.APPOS_SUPERUSER_EMAIL }} | ||
| APPOS_SUPERUSER_PASSWORD: ${{ secrets.APPOS_SUPERUSER_PASSWORD }} | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| - uses: actions/setup-node@v5 | ||
| with: | ||
| node-version: '24' | ||
| cache: npm | ||
| cache-dependency-path: tests/package-lock.json | ||
|
|
||
| - name: Install browser test deps | ||
| run: cd tests && npm ci | ||
|
|
||
| - name: Install Playwright Browsers | ||
| run: cd tests && npx playwright install --with-deps chromium | ||
|
|
||
| - name: Run Playwright tests | ||
| run: cd tests && npx playwright test -c playwright.config.ts | ||
|
|
||
| - uses: actions/upload-artifact@v6 | ||
| if: ${{ !cancelled() }} | ||
| with: | ||
| name: playwright-report | ||
| path: tests/playwright-report/ | ||
| retention-days: 30 |
| name: Run Staging Gate | ||
| uses: ./.github/workflows/_quality-gate.yml | ||
| with: | ||
| gate_stage: staging |
| "Prefer crafting your own tools over depending on external ones. A script you wrote " | ||
| "and saved is more reliable than an external API. Use the file system creatively.", |
| except BaseException: | ||
| try: | ||
| os.unlink(tmp) | ||
| except OSError: |
| except BaseException: | ||
| try: | ||
| os.unlink(tmp) | ||
| except OSError: |
| module_description = stripped.split(":", 1)[1].strip().strip('"').strip("'") | ||
| elif stripped.startswith("module_version:"): | ||
| module_version = stripped.split(":", 1)[1].strip().strip('"').strip("'") | ||
| except Exception: |
| Run with: python3 -m pytest test_canon_sync.py | ||
| (or plain `python3 test_canon_sync.py` for a lightweight self-check). | ||
| """ | ||
| import sys |
| } | ||
| continue | ||
| } | ||
| if (value === undefined || value === null || value === '') { |
| ))} | ||
| </div> | ||
| </> | ||
| ) : templateKey ? ( |
| offset += page.items.length | ||
| } | ||
|
|
||
| return ( |
| limit: pageLimit, | ||
| offset, | ||
| }) | ||
| lastPage = page |
| if err != nil { | ||
| return Entry{}, err | ||
| } | ||
| defer out.Close() |
| return Entry{}, err | ||
| } | ||
| if limited && written > maxBytes { | ||
| out.Close() |
| } | ||
|
|
||
| logf("docker image pull started: " + image) | ||
| pullErr := error(nil) |
| Summary: body.Summary, | ||
| FaviconURL: body.FaviconURL, | ||
| }) | ||
| normalized.OriginType = feeds.OriginTypeBookmark |
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
PR Summary by QodoDev to main: agent workers, AI providers, feeds, monitoring, cron & CI overhaul
AI Description
Diagram
High-Level Assessment
Files changed (15)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
70 rules 1. clusterId field added
|
| "placeholder": "appos" | ||
| }, | ||
| { | ||
| "id": "clusterId", |
There was a problem hiding this comment.
1. clusterid field added 📘 Rule violation ⌂ Architecture
The new Kafka-compatible instance template introduces a clusterId field, which encodes a multi-node/distributed topology assumption in config. This conflicts with the single-instance deployment compliance requirement and may lead to unsupported distributed behaviors being configured.
Agent Prompt
## Issue description
The new `kafka-compatible/_template.json` adds a `clusterId` field, which implies a distributed/cluster deployment assumption.
## Issue Context
Compliance requires avoiding cluster/distributed-architecture assumptions in code/config.
## Fix Focus Areas
- backend/domain/resource/instances/templates/kafka-compatible/_template.json[18-21]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
|
||
| Reference source: | ||
|
|
||
| - [NPM]( |
There was a problem hiding this comment.
2. Nginx product name in docs 📘 Rule violation § Compliance
A new public-facing README references NginxProxyManager, which is a specific product name rather than a generic technology role. This can make documentation vendor-specific where vendor neutrality is required.
Agent Prompt
## Issue description
The DNS templates README references `NginxProxyManager` (vendor/product-specific) instead of using a generic description.
## Issue Context
Compliance requires generic technology terms in public-facing documentation unless specificity is necessary for an explicit migration/integration guide.
## Fix Focus Areas
- backend/domain/resource/connectors/templates/dns/README.md[5-6]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| forbidOnly: !!process.env.CI, | ||
| retries: process.env.CI ? 2 : 0, | ||
| workers: process.env.CI ? 1 : undefined, | ||
| reporter: 'html', |
There was a problem hiding this comment.
3. Playwright reporter missing junit 📜 Skill insight ◔ Observability
tests/playwright.config.ts sets reporter: 'html' instead of configuring HTML + JUnit + a console reporter. This prevents generating standardized CI artifacts and reduces test observability.
Agent Prompt
## Issue description
Playwright config uses a single `html` reporter, but compliance requires HTML + JUnit + a console reporter.
## Issue Context
CI pipelines typically rely on JUnit output for test result ingestion and a console reporter for readable logs.
## Fix Focus Areas
- tests/playwright.config.ts[13-19]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| workers: process.env.CI ? 1 : undefined, | ||
| reporter: 'html', | ||
| use: { | ||
| baseURL: process.env.APPOS_BASE_URL, |
There was a problem hiding this comment.
4. Playwright trace retention misconfigured 📜 Skill insight ➹ Performance
The Playwright config uses trace: 'on-first-retry' instead of the required retain-on-failure setting. This can cause missing traces for failed tests and complicate failure triage.
Agent Prompt
## Issue description
Playwright `trace` is not set to the required failure-only retention mode.
## Issue Context
Compliance standardizes artifact retention to minimize storage while ensuring debugging artifacts exist for failures.
## Fix Focus Areas
- tests/playwright.config.ts[15-18]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
|
||
| const testsRoot = path.dirname(fileURLToPath(import.meta.url)) | ||
|
|
||
| export default defineConfig({ |
There was a problem hiding this comment.
5. Playwright timeouts not standardized 📜 Skill insight ☼ Reliability
The new tests/playwright.config.ts does not set the required standardized timeouts (timeout, actionTimeout, navigationTimeout, expect.timeout). This can lead to inconsistent behavior and flaky tests across environments.
Agent Prompt
## Issue description
Playwright config is missing the standardized timeout settings required by compliance.
## Issue Context
Standard timeouts are needed for consistent, reliable CI behavior.
## Fix Focus Areas
- tests/playwright.config.ts[7-19]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| superuserStorageState: string | ||
| } | ||
|
|
||
| const email = process.env.APPOS_SUPERUSER_EMAIL || 'admin@websoft9.com' |
There was a problem hiding this comment.
6. Hardcoded superuser credential defaults 📜 Skill insight ⛨ Security
Test fixtures include fallback superuser email/password literals (admin@websoft9.com, changeme123) instead of sourcing credentials exclusively from environment variables. This risks committing real or guessable credentials and can cause unsafe defaults in test runs.
Agent Prompt
## Issue description
The E2E fixture hardcodes default superuser credentials as fallbacks when env vars are absent.
## Issue Context
Compliance forbids hardcoded credentials/secrets in test configuration and test code.
## Fix Focus Areas
- tests/e2e/fixtures/appos.ts[51-52]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| await page.getByRole('button', { name: 'Run Now' }).click() | ||
|
|
||
| const approveButton = page.getByRole('button', { name: 'Approve' }) | ||
| const hasApprove = await approveButton.isVisible().catch(() => false) |
There was a problem hiding this comment.
7. if/else branches in test 📜 Skill insight ☼ Reliability
The new E2E test branches on runtime UI state (if (hasApprove) ... else ...), producing non-deterministic execution paths and potentially hiding regressions. Tests must follow a single deterministic path without conditional branching.
Agent Prompt
## Issue description
The E2E test uses `if/else` branching to decide what to assert/do based on whether UI elements are visible.
## Issue Context
Compliance requires deterministic test flow without conditional branching inside test bodies.
## Fix Focus Areas
- tests/e2e/workflows-smoke.spec.ts[108-114]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| "github.com/websoft9/appos/backend/infra/egress" | ||
| ) | ||
|
|
||
| func IsOpenRouterEndpoint(endpoint string) bool { |
There was a problem hiding this comment.
8. Openrouter endpoint spoofing 🐞 Bug ⛨ Security
IsOpenRouterEndpoint uses a substring check, so attacker-controlled endpoints (e.g., openrouter.ai.evil.com or URLs with openrouter.ai in the path) can be misclassified as OpenRouter and trigger credential validation. ValidateOpenRouterCredential then sends Authorization: Bearer <apiKey> to endpoint + "/auth/key", enabling API key exfiltration and/or SSRF depending on who can configure the endpoint.
Agent Prompt
### Issue description
`IsOpenRouterEndpoint()` treats any endpoint string containing `openrouter.ai` as OpenRouter. This allows malicious endpoints to be classified as OpenRouter, after which `ValidateOpenRouterCredential()` will build a request URL from the untrusted endpoint and transmit the stored API key in an `Authorization: Bearer ...` header.
### Issue Context
This impacts credential validation and error-path revalidation flows, where endpoints are read from provider configuration and then used to construct `authURL := endpoint + "/auth/key"`.
### Fix Focus Areas
- backend/domain/ai/copilot/openrouter_auth.go[14-72]
- backend/domain/routes/ai_providers.go[735-749]
- backend/domain/ai/copilot/service.go[201-207]
### What to change
1. Replace substring matching with URL parsing and strict hostname validation:
- Parse the endpoint as a URL (optionally prepend `https://` if missing).
- Compare `u.Hostname()` against `openrouter.ai` or `*.openrouter.ai` using `host == "openrouter.ai" || strings.HasSuffix(host, ".openrouter.ai")`.
- Optionally require `https` scheme for OpenRouter.
2. In `ValidateOpenRouterCredential`, consider rejecting validation if hostname is not an allowed OpenRouter hostname (defense-in-depth), even if callers mistakenly invoke it.
3. Add tests covering `openrouter.ai.evil.com`, `127.0.0.1/openrouter.ai`, and scheme-less endpoints.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if err != nil { | ||
| return err | ||
| } | ||
| req.Header.Set("Accept", "application/json") |
There was a problem hiding this comment.
9. Auth header overridden 🐞 Bug ≡ Correctness
validateOpenRouterCredentialWithClient sets Authorization: Bearer <apiKey> and then blindly applies the caller-provided headers map, allowing Authorization to be overwritten and causing the validation request to use the wrong auth scheme. In the error-path validation flow, service.go passes providerHeaders(provider), which can set non-Bearer Authorization values, making OpenRouter validation misleading/incorrect.
Agent Prompt
### Issue description
OpenRouter validation intends to verify the provided `apiKey` as a Bearer token, but the code applies caller headers *after* setting `Authorization`, so a caller-supplied `Authorization` (e.g., `Basic ...`) replaces the Bearer token and invalidates the check.
### Issue Context
`Service` calls `ValidateOpenRouterCredential(..., providerHeaders(provider), provider.APIKey)` on runtime failures. `providerHeaders` is derived from provider config and may set `Authorization` based on `AuthScheme`.
### Fix Focus Areas
- backend/domain/ai/copilot/openrouter_auth.go[40-52]
- backend/domain/ai/copilot/service.go[201-205]
- backend/domain/ai/copilot/eino_model.go[170-232]
### What to change
1. Prevent overriding security-critical headers in OpenRouter validation:
- Either apply `headers` first and then set `Authorization` last, or
- Explicitly ignore `Authorization` (and optionally `Host`) keys from the `headers` map when building the OpenRouter validation request.
2. Narrow allowed extra headers for OpenRouter validation to a safe allowlist (e.g., `HTTP-Referer`, `X-Title`, `User-Agent`).
3. Add a test proving that even when `headers["Authorization"]` is set, the final request uses `Bearer <apiKey>`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
chendelin1982
left a comment
There was a problem hiding this comment.
suggest merge now
Summary
All commits