diff --git a/.github/workflows/ai-policy.yml b/.github/workflows/ai-policy.yml new file mode 100644 index 00000000000..4ddfe34775f --- /dev/null +++ b/.github/workflows/ai-policy.yml @@ -0,0 +1,172 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: AI Policy + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [master, main] + +permissions: + contents: read + # Required to add the "AI assisted" label via `gh pr edit --add-label` + pull-requests: write + # Required to create the "AI assisted" label via the REST labels endpoint + # (labels are an issues-scoped resource in the GitHub API) + issues: write + +concurrency: + group: ai-policy-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + check-ai-trailers: + runs-on: ubuntu-latest-low + steps: + - name: Collect PR commit messages + id: collect + env: + GH_TOKEN: ${{ github.token }} + COMMITS_URL: ${{ github.event.pull_request.commits_url }} + run: | + set -euo pipefail + gh api ${COMMITS_URL} | jq -r '.[] | .commit.message' > /tmp/pr_commits.txt + echo "--- PR commit messages ---" + cat /tmp/pr_commits.txt + echo "--------------------------" + + - name: Define shared agent detection patterns + run: | + set -euo pipefail + + # Email addresses known to be used by coding agents. + # These should never appear in Signed-off-by because the DCO can only be attested by a human. + EMAIL_PATTERN="copilot@github\.com\ + |noreply@anthropic\.com\ + |devin@cognition\.ai\ + |devin@cognition-labs\.com\ + |aider@aider\.chat\ + |noreply@aider\.chat\ + |codex@openai\.com\ + |cursor@anysphere\.com\ + |windsurf@codeium\.com\ + |codeium@codeium\.com\ + |amazon-q@amazon\.com\ + |codewhisperer@amazon\.com\ + |gemini-code-assist@google\.com\ + |openhands@all-hands\.dev\ + |swe-agent@princeton\.edu" + + # Strip embedded whitespace (used above only for readability) + EMAIL_PATTERN=$(echo "$EMAIL_PATTERN" | tr -d ' \n') + echo "AGENT_EMAIL_PATTERN=${EMAIL_PATTERN}" >> "$GITHUB_ENV" + + # Display-name prefixes used by known coding agents (shared by Signed-off-by and Co-Authored-By checks) + # shellcheck disable=SC2016 + echo 'AGENT_NAMES=GitHub Copilot|Claude( [A-Za-z0-9. -]+)?|Devin( AI)?|aider( \(.*\))?|OpenAI Codex|Cursor( AI)?|Windsurf|Amazon Q|CodeWhisperer|Gemini Code Assist|OpenHands|SWE-agent|AutoCodeRover|Tabnine' >> "$GITHUB_ENV" + + - name: Check for AI-assistant / Assisted-by trailers + id: ai_trailers + run: | + set -euo pipefail + AI_ASSISTED=false + if grep -qiE '^(AI-assistant|Assisted-by|AI-Assisted-By):' /tmp/pr_commits.txt; then + AI_ASSISTED=true + echo "Found AI-assistant/Assisted-by/AI-Assisted-By trailer(s):" + grep -iE '^(AI-assistant|Assisted-by|AI-Assisted-By):' /tmp/pr_commits.txt + fi + echo "ai_assisted=${AI_ASSISTED}" >> "$GITHUB_OUTPUT" + + - name: Check for coding-agent Signed-off-by trailers + id: agent_signoff + run: | + set -euo pipefail + + EMAIL_HITS=$(grep -iE "^Signed-off-by:.*<(${AGENT_EMAIL_PATTERN})>" /tmp/pr_commits.txt 2>/dev/null || true) + NAME_HITS=$(grep -iE "^Signed-off-by: *(${AGENT_NAMES}) *[<(]" /tmp/pr_commits.txt 2>/dev/null || true) + + AGENT_LINES=$(printf '%s\n%s' "$EMAIL_HITS" "$NAME_HITS" | sort -u | sed '/^[[:space:]]*$/d') + + AGENT_SIGNOFF=false + if [ -n "$AGENT_LINES" ]; then + AGENT_SIGNOFF=true + fi + + echo "agent_signoff=${AGENT_SIGNOFF}" >> "$GITHUB_OUTPUT" + { + echo "agent_lines<> "$GITHUB_OUTPUT" + + - name: Check for coding-agent Co-Authored-By trailers + id: co_authored + run: | + set -euo pipefail + + EMAIL_HITS=$(grep -iE "^Co-Authored-By:.*<(${AGENT_EMAIL_PATTERN})>" /tmp/pr_commits.txt 2>/dev/null || true) + NAME_HITS=$(grep -iE "^Co-Authored-By: *(${AGENT_NAMES}) *[<(]" /tmp/pr_commits.txt 2>/dev/null || true) + + CO_AUTHORED=false + if [ -n "$EMAIL_HITS" ] || [ -n "$NAME_HITS" ]; then + CO_AUTHORED=true + echo "Found coding-agent Co-Authored-By trailer(s):" + printf '%s\n%s' "$EMAIL_HITS" "$NAME_HITS" | sort -u | sed '/^[[:space:]]*$/d' + fi + + echo "co_authored=${CO_AUTHORED}" >> "$GITHUB_OUTPUT" + + - name: Create 'AI assisted' label if absent + if: steps.ai_trailers.outputs.ai_assisted == 'true' || steps.agent_signoff.outputs.agent_signoff == 'true' || steps.co_authored.outputs.co_authored == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api "repos/${{ github.repository }}/labels" \ + --method POST \ + -f name="AI assisted" \ + -f color="d93f0b" \ + -f description="This PR contains AI-assisted commits" \ + 2>/dev/null || true + + - name: Label PR as AI assisted + if: steps.ai_trailers.outputs.ai_assisted == 'true' || steps.agent_signoff.outputs.agent_signoff == 'true' || steps.co_authored.outputs.co_authored == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr edit "${{ github.event.pull_request.number }}" \ + --repo "${{ github.repository }}" \ + --add-label "AI assisted" + echo "Added 'AI assisted' label to PR #${{ github.event.pull_request.number }}" + + - name: Fail on coding-agent Signed-off-by + if: steps.agent_signoff.outputs.agent_signoff == 'true' + env: + AGENT_LINES: ${{ steps.agent_signoff.outputs.agent_lines }} + AGENTS_MD_URL: https://github.com/${{ github.repository }}/blob/${{ github.base_ref }}/AGENTS.md + run: | + echo "::error title=Coding-agent sign-off detected::A Signed-off-by trailer from a known coding agent was found in one or more commits." + echo "" + echo "Offending trailer(s):" + echo "${AGENT_LINES}" + echo "" + echo "The 'Signed-off-by' trailer represents the Developer Certificate of Origin (DCO)" + echo "and must only be attested by a human contributor." + echo "Please amend the affected commit(s) to remove the coding-agent sign-off" + echo "and replace it with an 'Assisted-by' trailer, for example:" + echo "" + echo " Assisted-by: Claude Code:claude-sonnet-4-6" + echo "" + echo "References:" + echo " • AGENTS.md (this repository)" + echo " ${AGENTS_MD_URL}" + echo " • AI Contribution Policy" + echo " https://github.com/nextcloud/.github/blob/master/AI_POLICY.md" + echo " • Contribution Guidelines" + echo " https://github.com/nextcloud/.github/blob/master/CONTRIBUTING.md" + exit 1 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..4ee91aab232 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,126 @@ + +# AGENTS.md — Nextcloud Talk (spreed) + +Guidance for AI coding agents in this repo. PHP backend in `lib/`, Behat integration tests in `tests/integration/`, unit tests in `tests/php/`. Vue 3 + TS frontend in `src/`, tests colocated as `*.spec.js`/`*.spec.ts`. + +## Contribution policy + +Comply with the [AI Contribution Policy](https://github.com/nextcloud/.github/blob/master/AI_POLICY.md) (disclosure, accountability, security, licensing, code quality, autonomous behavior) and [Contribution Guidelines](https://github.com/nextcloud/.github/blob/master/CONTRIBUTING.md) (testing, DCO, license headers, conventional commits, translations). + +**Always:** +- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to every AI-assisted commit. +- Disclose AI tool use in every PR description. +- Keep PRs focused on one concern; no unrelated files or incidental refactors. +- Verify dependencies exist in real package registries. +- Tell the contributor when an action would violate the policy/guidelines — name the rule and the alternative; never silently proceed. +- Warn before a PR grows too large (approaching several thousand lines) and suggest a split. +- Recommend a discussion ticket before complex changes (multiple subsystems, architectural decisions, unclear approach). + +**Never:** +- Open issues, submit PRs, post review comments, or send security reports autonomously — a human submits every contribution. +- Add `Signed-off-by` tags (only the human certifies the DCO). +- Submit unverified security reports; report verified ones via [HackerOne](https://hackerone.com/nextcloud), not GitHub issues. +- Write PR descriptions, review comments, or issue reports for the contributor — these must be in their own words. +- Fully automate resolution of [`good first issue`](https://github.com/issues?q=org%3Anextcloud+label%3A%22good+first+issue%22)-style issues. +- Submit unreviewed code — remove dead code, redundant logic, excessive comments, and unrelated changes first. + +## Baseline & tooling + +- `l10n/` is generated from Transifex (`fix(l10n): Update translations…` commits). **Never hand-edit translation files.** + +**Backend:** PHP **8.2+**, Nextcloud server **35** (`appinfo/info.xml`). Stay portable across MariaDB, MySQL, PostgreSQL, SQLite, Oracle (migrations, query builder, tests). +- Setup: `composer i`. `lib/Vendor/` is Mozart-bundled third-party code (`cuyz/valinor`, `firebase/php-jwt`) — **never edit or analyze it.** +- Checks: `composer cs:fix`, `composer psalm`, `composer rector:fix`, `composer lint`. +- OpenAPI: docblocks/psalm types + `lib/ResponseDefinitions.php` feed `composer openapi` (also regenerates TS types). Regenerate after adding/removing/renaming a route, changing a controller signature or its `@param`/`@return`/psalm-shape, changing a response shape, or adding/removing an HTTP status code. CI fails on a stale spec or `src/types/openapi/`. + +**Frontend:** Vue **3.5** + TypeScript, built with **rspack** (no Vite). State: **Pinia** (`src/stores/`, target) and legacy **Vuex 4** (`src/store/`, being phased out). UI components from `@nextcloud/vue` only. +- Setup: `npm ci`. Build: `npm run build` (prod), `npm run dev`/`npm run watch`. +- Checks: `npm run lint:fix`, `npm run stylelint`, `npm run ts:check`, `npm run test` (vitest + @vue/test-utils v2; no jest). +- OCS/API types generated via `npm run ts:generate` (openapi-typescript) into `src/types/openapi/` — regenerate, don't hand-edit. +- No compat mode, mixins, `mapGetters`/`$set`/`::v-deep` — removed in the Vue 2→3 migration; don't reintroduce. + +## Running tests + +- PHP unit (`tests/php/`): `composer run test:unit`. Single file: `composer run test:unit -- tests/php/Service/AvatarServiceTest.php`; filter: `composer run test:unit -- --filter="testMethodName"`. +- `tests/php/bootstrap.php` requires `../../../../lib/base.php`: the suite only runs when this repo lives at `/apps/spreed`. **If you cannot run a suite, say so — don't claim it passes.** `composer lint`/`composer psalm` run standalone. +- Integration (Behat, `tests/integration/`): `run.sh` against a local server, or `run-docker.sh`. +- Frontend: `npm run test`; single file via `npm run test -- `. + +## License headers + +Every new file needs an SPDX header. Use `AGPL-3.0-or-later`, never `AGPL-3.0-only`: + +```php +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +``` + +Adapt the comment style to the file type; files that can't carry a header (binary assets) go in `REUSE.toml`. CI enforces REUSE. + +## Git workflow + +- Do **not** commit or push unless explicitly asked. Leave changes in the working dir, summarize, and suggest a commit message. +- Never commit/push to `main` — use a `type/issue-or-noid/short-description` branch (e.g. `fix/12345/handle-mcu-disconnect`), not generated names like `agent-xxxx`. +- Conventional Commits with a component scope: `feat(chat): …`, `fix(call): …`, `fix(api): …`. +- Backports happen via a `/backport to stable-X.Y` comment on the merged PR — don't cherry-pick manually. + +## Backend conventions (modern pattern first; avoid the drift) + +From a 2026-06 tech-debt analysis. New code uses the modern pattern. + +- **DI:** constructor injection with property promotion. Don't add `\OCP\Server::get()` calls — existing ones are debt (the service-locator blocks in `lib/Room.php` where `getName()` even writes to the DB; ~42 `Server::get()` calls instantiating federation proxy controllers, 13 in `ChatController`). +- **Data access:** new persisted entities use `QBMapper` + `SnowflakeAwareEntity` in `lib/Model/` (templates: `ConversationTag`, `ScheduledMessage`). `Room`/`Participant` are **not** Entities — hand-hydrated in `Manager::createRoomObject()` from columns aliased in `lib/Model/SelectHelper.php`; adding a column means touching the migration, `SelectHelper`, `Manager` hydration, and the constructor **in lockstep**. Query builder only (no raw SQL outside migrations). Build queries **outside** loops via `createParameter`/`setParameter`; chunk `IN ()` with `array_chunk(…, IQueryBuilder::MAX_IN_PARAMETERS)` for Oracle and `array_merge(...$results)` once after the loop (see `lib/Model/ThreadMapper.php`). +- **Services:** reads/lookups in `lib/Manager.php`; writes/mutations + event dispatch in `lib/Service/RoomService.php`/`ParticipantService.php` — keep the split. New services in `lib/Service/` named `*Service`. Lib-root `GuestManager`, `MatterbridgeManager`, `lib/Chat/ChatManager` are legacy naming. Use `lib/Federation/`, `RecordingService`, `BotService`, `lib/RoomPresets/` as templates, not 2016-era core. +- **Errors:** lookups throw domain exceptions (`RoomNotFoundException`, `ParticipantNotFoundException` in `lib/Exceptions/`). Idempotent setters may return `bool` (false = no-op), e.g. `RoomService::setPermissions()`; don't return `null`/`false` for "not found". +- **Events:** typed only (`dispatchTyped()`), extending the `A`-prefixed bases in `lib/Events/`, with `Before*`/`*` pairs for mutations; registered in `lib/AppInfo/Application.php`. No string events/hooks. +- **Controllers/API:** OCS controllers needing room/participant context extend `AEnvironmentAwareOCSController` (populated by `InjectionMiddleware` via `#[RequireRoom]`-style attributes in `lib/Middleware/Attribute/`); others extend `OCSController`. PHP attributes only (`#[NoAdminRequired]`, `#[PublicPage]`, `#[BruteForceProtection]`, `#[ApiRoute]`) — never docblock annotations. Responses are `DataResponse` with psalm shapes from `ResponseDefinitions.php`. +- **Config:** settings go through the `lib/Config.php` facade; register new app-config keys in `lib/ConfigLexicon.php` (the emerging registry). +- **Caching:** cache prefixes belong in `lib/CachePrefix.php` — don't add ad-hoc prefixes (drift: `hpb_servers` lacks `talk/`, `Capabilities.php` uses raw `'talk::'`). +- **PHP style:** strict comparisons, `?Type` nullables, `match` over `switch`, `str_contains`/`str_starts_with`, `readonly` where applicable, arrow functions, `JSON_THROW_ON_ERROR` on new json calls. Prefer native backed enums for new value sets (only `lib/RoomAttributes.php`, `lib/RoomPresets/Parameter.php` exist today); bitflags (`Attendee::PERMISSIONS_*`, `Participant::FLAG_*`) stay int constants. + +## Frontend conventions (modern pattern first; avoid the drift) + +- **Components:** new/rewritten SFCs use `