Skip to content

Dev to main#3

Merged
chendelin1982 merged 78 commits into
mainfrom
dev
Jul 20, 2026
Merged

Dev to main#3
chendelin1982 merged 78 commits into
mainfrom
dev

Conversation

@chendelin1982

Copy link
Copy Markdown
Contributor

Summary

All commits

Copilot AI review requested due to automatic review settings July 20, 2026 08:29
Comment on lines -12 to +151
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
Comment on lines +15 to +18
name: Run Post-Merge Gate
uses: ./.github/workflows/_quality-gate.yml
with:
gate_stage: merge
Comment on lines +13 to +16
name: Run PR Gate
uses: ./.github/workflows/_quality-gate.yml
with:
gate_stage: pr
Comment on lines +8 to +38
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
Comment on lines +12 to +15
name: Run Staging Gate
uses: ./.github/workflows/_quality-gate.yml
with:
gate_stage: staging
Comment on lines +170 to +171
"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

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Dev to main: agent workers, AI providers, feeds, monitoring, cron & CI overhaul

✨ Enhancement 🐞 Bug fix ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Massive devmain merge spanning backend, frontend, CI, and BMAD agent skill assets.
• Replace standalone agent with integrated monitoring/telemetry pipeline and rebuilt platform
 metrics.
• Add major new product domains: AI copilot/providers, feeds ingestion/polling, managed server cron,
 and connectors/templates.
Diagram

graph TD
  A["appos main.go"] --> B["runtimecfg Resolver"] --> C["bootstrap.Initialize"]
  C --> D["worker.Worker (asynq)"]
  C --> E["routes.Register"]
  E --> F["AI Copilot / Providers"]
  E --> G["Feeds Poller"]
  E --> H["Server Cron Service"]
  E --> I["Monitor Signals & Telemetry"]
  D --> I
  H --> J[("PocketBase DB")]
  I --> K[("TSDB / VictoriaMetrics")]
  subgraph Legend
    direction LR
    _svc(["Service"]) ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split into concern-based PRs
  • ➕ Focused review per domain (runtime/bootstrap vs feeds vs AI vs CI)
  • ➕ Easier rollback/bisect when a regression appears
  • ➕ Allows merging low-risk infra/docs independently
  • ➖ More coordination to sequence merges
  • ➖ Extra CI runs and branch management
2. Merge as-is but gate with a release checklist
  • ➕ Preserves existing dev branch history and avoids rework
  • ➕ Single merge point for mainline
  • ➖ Review remains shallow due to size
  • ➖ Harder to attribute failures to a subsystem
  • ➖ Higher operational risk for production release

Recommendation: Prefer splitting: (1) runtimecfg/bootstrap + PB upgrade, (2) monitoring/telegraf/agent replacement, (3) feeds domain, (4) server cron + connector/resource templates, (5) CI/Makefile gates, and (6) BMAD agent assets. If merging as one, require a clear smoke/acceptance checklist and staged rollout since this PR is too large to confidently review line-by-line.

Files changed (15) +1731 / -816 · 4 not counted

Enhancement (6) +829 / -0
initialize.goAdd centralized bootstrap.Initialize and superuser auto-provisioning +103/-0

Add centralized bootstrap.Initialize and superuser auto-provisioning

• Introduces a single initialization path that ensures secrets runtime key, loads secrets/cert templates, ensures collections, and optionally creates/updates a configured superuser when init-mode=auto.

backend/cmd/appos/bootstrap/initialize.go

config.goNew runtimecfg package for YAML + flag overrides not counted

New runtimecfg package for YAML + flag overrides

• Adds Config defaults, YAML config loading, flag parsing/overrides, derived PocketBase data dir defaults, and helpers for web bundle discovery, Redis/TSDB URLs, tunnel SSH port, and init mode.

backend/domain/runtimecfg/config.go

routes.goRestructure API routing and add new route groups not counted

Restructure API routing and add new route groups

• Adds /api/system group, wires in branding/media/assets/workflow/feeds/AI copilot + agent routes, registers docker routes under /api/servers, and introduces self-proxy ingress and public traefik routes.

backend/domain/routes/routes.go

poller.goImplement feed polling and ingestion pipeline +188/-0

Implement feed polling and ingestion pipeline

• Adds due/forced polling over feed sources, fetch+parse, upsert of items keyed by external_id, status updates on success/failure, retention trimming per source cap, and item-count refresh.

backend/domain/feeds/poller.go

cron_service.goAdd managed cron job service (crontab registry + validation) +479/-0

Add managed cron job service (crontab registry + validation)

• Introduces ManagedCronService and supporting parsing/serialization for managed cron entries, including schedule validation, command constraints, and CRUD operations via a repository.

backend/domain/resource/servers/service/cron_service.go

bmad_loop_hook.pyAdd bmad-loop hook relay script for multiple coding CLIs +59/-0

Add bmad-loop hook relay script for multiple coding CLIs

• New stdlib-only Python hook that writes normalized session events to the BMAD loop run directory when BMAD env vars are present; no-ops outside orchestrated sessions.

.bmad-loop/bmad_loop_hook.py

Refactor (1) +61 / -31
main.goRefactor entrypoint for runtimecfg + conditional worker startup +61/-31

Refactor entrypoint for runtimecfg + conditional worker startup

• Resolves runtime config early, delegates initialization to bootstrap.Initialize, and only creates/starts the asynq worker and platform observer for serve mode. Also registers runtimecfg flags on PocketBase root command.

backend/cmd/appos/main.go

Documentation (2) +7 / -1
README.mdDocument docker run install snippet +6/-0

Document docker run install snippet

• Adds an Install section with a docker run example for the appos demo image.

README.md

README.mdUpdate OpenAPI generation docs for software/local routes +1/-1

Update OpenAPI generation docs for software/local routes

• Adjusts documented openapi-gen route coverage to include /api/software/local* generation coverage.

backend/README.md

Other (6) +834 / -784
go.modUpgrade PocketBase and add AI/cron/mail dependencies +52/-21

Upgrade PocketBase and add AI/cron/mail dependencies

• Bumps Go version (1.26.5), PocketBase (0.39.0), and adds CloudWeGo Eino + OpenAI ext, cron/v3, mailyak, redis client, and updates multiple golang.org/x dependencies.

backend/go.mod

MakefileReplace legacy check/lint flows with staged gates and local env support +674/-602

Replace legacy check/lint flows with staged gates and local env support

• Adds gate/qa/sec/test-env targets, init-env for local proxy env file, image pull mirror settings, opencode helpers, and reworks help/PHONY targets around pr/merge/staging/release gate stages.

Makefile

_quality-gate.ymlConsolidate quality gate into parameterized staged gate runner +103/-158

Consolidate quality gate into parameterized staged gate runner

• Replaces multiple jobs with a single gate job parameterized by gate_stage, updates actions versions, moves Node to 24, and conditionally installs govulncheck/gitleaks/playwright/syft depending on stage.

.github/workflows/_quality-gate.yml

pr-gate.ymlCall reusable gate runner with pr stage +5/-3

Call reusable gate runner with pr stage

• Renames the invoked job and passes gate_stage: pr into the reusable workflow.

.github/workflows/pr-gate.yml

vite.config.tsImprove bundle splitting for icons and document libs not counted

Improve bundle splitting for icons and document libs

• Adds manualChunks routing for lucide-react, pdfjs-dist, and mammoth bundles.

web/vite.config.ts

package.jsonAdd PDF/DOCX/Excel extraction deps and refresh tooling not counted

Add PDF/DOCX/Excel extraction deps and refresh tooling

• Adds mammoth, pdfjs-dist, read-excel-file, remark-gfm, user-event; upgrades vite/vitest/jsdom/js-yaml and adds overrides for security/compatibility.

web/package.json

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📜 Skill insights (5)

Context used
✅ Compliance rules (platform): 70 rules

Grey Divider


Action required

1. clusterId field added 📘 Rule violation ⌂ Architecture
Description
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.
Code

backend/domain/resource/instances/templates/kafka-compatible/_template.json[R18-21]

+      "id": "clusterId",
+      "label": "Cluster ID",
+      "type": "text",
+      "advanced": true
Evidence
PR Compliance ID 517575 disallows cluster/distributed-architecture assumptions in code/config. The
added clusterId field (label: Cluster ID) is an explicit cluster-topology detail introduced by
this PR.

Rule 517575: Disallow cluster or distributed-architecture assumptions in code and config
backend/domain/resource/instances/templates/kafka-compatible/_template.json[18-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


2. Playwright reporter missing JUnit 📜 Skill insight ◔ Observability
Description
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.
Code

tests/playwright.config.ts[13]

+  reporter: 'html',
Evidence
PR Compliance ID 1172347 requires the Playwright reporter array to include html, junit, and a
console reporter (e.g. list). The new config sets reporter: 'html' only.

tests/playwright.config.ts[13-13]
Skill: bmad-testarch-framework

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


3. Playwright timeouts not standardized 📜 Skill insight ☼ Reliability
Description
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.
Code

tests/playwright.config.ts[R7-19]

+export default defineConfig({
+  testDir: path.resolve(testsRoot, './e2e'),
+  fullyParallel: true,
+  forbidOnly: !!process.env.CI,
+  retries: process.env.CI ? 2 : 0,
+  workers: process.env.CI ? 1 : undefined,
+  reporter: 'html',
+  use: {
+    baseURL: process.env.APPOS_BASE_URL,
+    trace: 'on-first-retry',
+    screenshot: 'only-on-failure',
+    video: 'retain-on-failure',
+  },
Evidence
PR Compliance ID 1172317 requires specific standardized timeout values in playwright.config.ts.
The config introduced by this PR defines use settings but does not set any of the required timeout
fields.

tests/playwright.config.ts[7-19]
Skill: bmad-testarch-framework

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


View more (3)
4. Hardcoded superuser credential defaults 📜 Skill insight ⛨ Security
Description
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.
Code

tests/e2e/fixtures/appos.ts[R51-52]

+const email = process.env.APPOS_SUPERUSER_EMAIL || 'admin@websoft9.com'
+const password = process.env.APPOS_SUPERUSER_PASSWORD || 'changeme123'
Evidence
PR Compliance ID 1172435 forbids hardcoded credentials/secrets in test files and requires sensitive
values to come from environment variables. The fixture defines fallback literal credentials via
process.env ... || '...'.

tests/e2e/fixtures/appos.ts[51-52]
Skill: bmad-testarch-framework

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


5. if/else branches in test 📜 Skill insight ☼ Reliability
Description
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.
Code

tests/e2e/workflows-smoke.spec.ts[R108-114]

+  const hasApprove = await approveButton.isVisible().catch(() => false)
+  if (hasApprove) {
+    await approveButton.click()
+    await expect(page.locator('body')).toContainText(/succeeded|manual_gate|status/i)
+  } else {
+    await expect(page.locator('body')).toContainText(/manual|node|status/i)
+  }
Evidence
PR Compliance ID 1172311 prohibits conditional logic (if/else/switch) controlling test flow. The
test explicitly branches with if (hasApprove) { ... } else { ... }, causing different behaviors
between runs/environments.

tests/e2e/workflows-smoke.spec.ts[108-114]
Skill: bmad-testarch-test-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


6. OpenRouter endpoint spoofing 🐞 Bug ⛨ Security
Description
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.
Code

backend/domain/ai/copilot/openrouter_auth.go[R14-55]

+func IsOpenRouterEndpoint(endpoint string) bool {
+	return strings.Contains(strings.ToLower(strings.TrimSpace(endpoint)), "openrouter.ai")
+}
+
+func ValidateOpenRouterCredential(ctx context.Context, client *http.Client, endpoint string, headers map[string]string, apiKey string) error {
+	if strings.TrimSpace(apiKey) == "" {
+		return nil
+	}
+	endpoint = strings.TrimSpace(endpoint)
+	if endpoint == "" {
+		return fmt.Errorf("OpenRouter endpoint is required")
+	}
+	directErr := validateOpenRouterCredentialWithClient(ctx, newDirectOpenRouterValidationClient(), endpoint, headers, apiKey)
+	if directErr == nil || isOpenRouterCredentialRejected(directErr) {
+		return directErr
+	}
+	if client == nil {
+		return directErr
+	}
+	proxyErr := validateOpenRouterCredentialWithClient(ctx, client, endpoint, headers, apiKey)
+	if proxyErr == nil || isOpenRouterCredentialRejected(proxyErr) {
+		return proxyErr
+	}
+	return proxyErr
+}
+
+func validateOpenRouterCredentialWithClient(ctx context.Context, client *http.Client, endpoint string, headers map[string]string, apiKey string) error {
+	authURL := strings.TrimRight(endpoint, "/") + "/auth/key"
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL, nil)
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Accept", "application/json")
+	req.Header.Set("Authorization", "Bearer "+apiKey)
+	for key, value := range headers {
+		if strings.TrimSpace(value) != "" {
+			req.Header.Set(key, value)
+		}
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
Evidence
The OpenRouter classifier is a raw substring match, and the validator constructs an auth URL
directly from the configured endpoint and always attaches the API key as a Bearer token. Call sites
use IsOpenRouterEndpoint(...) to decide whether to run this validation against configured provider
endpoints.

backend/domain/ai/copilot/openrouter_auth.go[14-52]
backend/domain/routes/ai_providers.go[735-748]
backend/domain/ai/copilot/service.go[201-205]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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



Remediation recommended

7. Nginx product name in docs 📘 Rule violation § Compliance
Description
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.
Code

backend/domain/resource/connectors/templates/dns/README.md[R5-6]

+- [NPM](
+https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/backend/certbot/dns-plugins.json)
Evidence
PR Compliance ID 517574 requires using generic technology terms in public-facing docs. The added
reference URL includes NginxProxyManager, introducing a vendor/product-specific name in
documentation.

Rule 517574: Use generic technology terms in public-facing docs instead of specific product names
backend/domain/resource/connectors/templates/dns/README.md[5-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


8. Playwright trace retention misconfigured 📜 Skill insight ➹ Performance
Description
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.
Code

tests/playwright.config.ts[R15-18]

+    baseURL: process.env.APPOS_BASE_URL,
+    trace: 'on-first-retry',
+    screenshot: 'only-on-failure',
+    video: 'retain-on-failure',
Evidence
PR Compliance ID 1172310 mandates trace: 'retain-on-failure-and-retries' (alongside failure-only
screenshots/videos). The new config sets trace: 'on-first-retry', which does not match the
required value.

tests/playwright.config.ts[15-18]
Skill: bmad-testarch-framework

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


9. Auth header overridden 🐞 Bug ≡ Correctness
Description
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.
Code

backend/domain/ai/copilot/openrouter_auth.go[R46-52]

+	req.Header.Set("Accept", "application/json")
+	req.Header.Set("Authorization", "Bearer "+apiKey)
+	for key, value := range headers {
+		if strings.TrimSpace(value) != "" {
+			req.Header.Set(key, value)
+		}
+	}
Evidence
The validator sets Authorization and then overwrites headers from a map; the service passes
providerHeaders(provider) into that map, and providerHeaders/authSchemeHeaders can emit
Authorization values that are not Bearer, so the validation request can end up using the wrong
Authorization header.

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-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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


Grey Divider

Qodo Logo

"placeholder": "appos"
},
{
"id": "clusterId",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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](

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

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({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

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 chendelin1982 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest merge now

@chendelin1982
chendelin1982 merged commit d25dbbd into main Jul 20, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants