From 5d65b8ef97e0b5b8e6d20fccefe6b0b0bfd886da Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:09:58 +0200 Subject: [PATCH 1/4] ci: resolve attestation eligibility from repository API (#106) * ci: resolve attestation eligibility from repository API The publish guard in #103 used github.repository_owner_type, which is not part of the documented github context and evaluates empty on push. That made the OR condition true and the attest step always ran. Query repos/{owner}/{repo} for owner.type and private, skip attestation only for User-owned private repositories, and gate the attest action on the resolved step output. Co-authored-by: cynarAI * test: escape workflow placeholders for Biome noTemplateCurlyInString Co-authored-by: cynarAI --------- Co-authored-by: Cursor Agent Co-authored-by: cynarAI --- .github/workflows/publish-server-image.yml | 18 ++++++++++++++++-- infra/updater/src/publish-workflow.test.ts | 14 ++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish-server-image.yml b/.github/workflows/publish-server-image.yml index dedea74..791958c 100644 --- a/.github/workflows/publish-server-image.yml +++ b/.github/workflows/publish-server-image.yml @@ -256,9 +256,23 @@ jobs: grep -Fqx "$want" <<< "$platforms" || { echo "$first_tag is missing $want" >&2; exit 1; } done echo "digest=$(docker buildx imagetools inspect "$first_tag" --format '{{.Manifest.Digest}}')" >> "$GITHUB_OUTPUT" - # GitHub artifact attestations are unavailable for private repos on personal accounts. + # Owner type is not exposed on the github context; resolve it from the repository metadata API. + - name: Resolve registry attestation eligibility + id: attestation + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + owner_type="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.owner.type')" + private="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.private')" + if [[ "$owner_type" == "User" && "$private" == "true" ]]; then + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "Skipping registry attestation: unavailable for private repositories on personal accounts." + else + echo "enabled=true" >> "$GITHUB_OUTPUT" + fi - name: Attest the published image - if: github.event.repository.private != true || github.repository_owner_type != 'User' + if: steps.attestation.outputs.enabled == 'true' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }} diff --git a/infra/updater/src/publish-workflow.test.ts b/infra/updater/src/publish-workflow.test.ts index f75de81..4c9e292 100644 --- a/infra/updater/src/publish-workflow.test.ts +++ b/infra/updater/src/publish-workflow.test.ts @@ -38,12 +38,14 @@ describe("server image publish workflow", () => { expect(publish.needs).toBe("build"); expect(workflowText).toContain("push-by-digest=true"); expect(workflowText).toContain("docker buildx imagetools create"); - expect(workflowText).toContain('if [[ "${#sources[@]}" -ne 1 ]]'); + expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`); expect(workflowText).toContain("for want in linux/amd64; do"); expect(workflowText).toContain("actions/attest-build-provenance@"); - expect(workflowText).toContain( - "if: github.event.repository.private != true || github.repository_owner_type != 'User'", - ); + expect(workflowText).not.toContain("github.repository_owner_type"); + expect(workflowText).toContain(`gh api "repos/\${GITHUB_REPOSITORY}"`); + expect(workflowText).toContain(".owner.type"); + expect(workflowText).toContain("if: steps.attestation.outputs.enabled == 'true'"); + expect(workflowText).toContain('[[ "$owner_type" == "User" && "$private" == "true" ]]'); }); it("stores digest markers under RUNNER_TEMP so self-hosted runners do not leak stale files", () => { @@ -52,11 +54,11 @@ describe("server image publish workflow", () => { }); it("names every image under the lowercase repository namespace", () => { - expect(workflowText).not.toContain("ghcr.io/${{ github.repository }}"); + expect(workflowText).not.toContain(`ghcr.io/\${{ github.repository }}`); const references = workflowText.match(/ghcr\.io\/\$\{\{ steps\.ns\.outputs\.repo \}\}\//g) ?? []; expect(references.length).toBeGreaterThanOrEqual(6); - expect(workflowText).toContain("repo=${GITHUB_REPOSITORY,,}"); + expect(workflowText).toContain(`repo=\${GITHUB_REPOSITORY,,}`); }); it("keeps pull requests read-only and every action pinned to a commit", () => { From f8eabee9972622118783c00ba206364bb199962d Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:56:44 +0200 Subject: [PATCH 2/4] fix(web): keep secondary shell text readable in the light theme (#117) Secondary shell text was muted text dimmed further with opacity: the section labels, the projects caption, the sidebar "+" icons, the privacy panel, the trading subtitle and the Heute brief's Mission Control link. Muted text measures 4.4:1 on the light sidebar, so these fell to 1.9 to 3.4:1 in the light theme; a few were below 4.5:1 in dark as well. - Secondary sidebar text and icons share `rk-sidebar-muted`, the sidebar foreground at 65% that row times already used (5.0:1 on the light sidebar). Row times now use the class too. - Section labels and space or project group titles use the dimmed foreground in light and keep the 80% muted color in dark; group titles move up from 60%, which was 3.4:1 in dark. - The privacy panel heading is plain text with a success-colored check instead of emerald text, and its details pass 4.5:1 in both themes. - Needs-you chips keep the warning tint and border; their label uses the foreground color in light. - The Mission Control link uses the full muted color. - E2E: the shell test checks every text in the sidebar except the logo, and the Heute brief, in both themes, and captures `shell-contrast-{light,dark}-heute`. - AGENTS.md: agents make routine calls themselves, verify the result and report it, and ask only before irreversible actions or for product decisions without a sensible default. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SSVQGR4WU92LaddTP19GcQ --- AGENTS.md | 1 + apps/web/e2e/chat-contrast.spec.ts | 58 ++++++++++--------- apps/web/src/components/DataTrustPanel.tsx | 12 ++-- .../src/components/TradingModeIndicator.tsx | 2 +- apps/web/src/pages/HeuteBrief.tsx | 4 +- apps/web/src/pages/Shell.tsx | 24 ++++---- apps/web/src/pages/shell/roster-row-style.ts | 4 +- apps/web/src/styles.css | 8 ++- 8 files changed, 60 insertions(+), 53 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 86604d5..58df55b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ - Keep code simple: reuse existing primitives and one source of truth, remove duplication and unused flexibility, and avoid speculative abstractions. Add an interface when it protects a real external or platform boundary, not for its own sake. - Treat auth, secret handling, sandbox boundaries, host commands, and integrations as security-sensitive. Keep tests deterministic and offline by default. - The desktop Playwright suite (`pnpm --filter @rakazo/desktop test:e2e`) opens real Electron windows and steals focus on macOS. Do not run it on a maintainer's machine as routine verification; run the unit tests locally and let CI's virtual-display job run the e2e on push. +- Work autonomously. Make routine calls yourself, such as opening or reopening a pull request, linking CI screenshots, or fixing gaps you found that belong to the task, then verify and report the outcome. Ask the maintainer only before irreversible actions or for product decisions without a sensible default. - After creating a pull request, stay with it until CI and automated review bots have finished; passing checks alone do not mean the review is complete. Follow `.agents/skills/pr-watch/SKILL.md`: run its `pr-digest --watch` helper as a single backgrounded call that blocks until the checks on the current head commit are terminal, then act on the verdict it prints. Do not poll in the foreground on a timer, which mostly buys repeated snapshots of an unchanged PR and dies at the tool timeout. Address every actionable issue, push the fixes, and repeat until no actionable feedback remains. Do not merge while review bots are still pending or review issues remain unresolved. - PR descriptions say why the change exists, what changed, and how it was tested. Write Why as the product reason (what was wrong and what this changes); never "Elie wants" or a third-person briefing of a named person; the maintainer is the author. - Commit messages, PR descriptions, issues, and review replies are public too. Describe test results in words instead of pasting tool output, and never include anything that identifies a person, machine, or account: local paths, usernames, hostnames, signing identities, legal entity names, Apple Team IDs, API key IDs, issuer or tenant IDs, account emails, or the accounts, machines, and files that secret values came from. Naming the secrets a workflow reads is fine; use placeholders when a value must be shown. diff --git a/apps/web/e2e/chat-contrast.spec.ts b/apps/web/e2e/chat-contrast.spec.ts index c950ab6..675f974 100644 --- a/apps/web/e2e/chat-contrast.spec.ts +++ b/apps/web/e2e/chat-contrast.spec.ts @@ -16,11 +16,12 @@ test.use({ viewport: { width: 1280, height: 1000 } }); /** * Texts in `locator` below WCAG AA against the surface they are painted on. With `inside`, every - * visible text and typed value within it counts, except in disabled controls, which WCAG exempts. + * visible text and typed value within it counts, except in disabled controls, which WCAG exempts, + * and in elements matching `skip`. */ -function unreadableTexts(locator: Locator, inside: boolean) { +function unreadableTexts(locator: Locator, inside: boolean, skip: string | null) { return locator.evaluate( - (root, { inside, min }) => { + (root, { inside, skip, min }) => { const context = document .createElement("canvas") .getContext("2d", { willReadFrequently: true }); @@ -81,7 +82,11 @@ function unreadableTexts(locator: Locator, inside: boolean) { }; const elements = inside ? [root, ...root.querySelectorAll("*")].filter( - (element) => ownText(element) && shown(element) && !element.closest(":disabled"), + (element) => + ownText(element) && + shown(element) && + !element.closest(":disabled") && + !(skip && element.closest(skip)), ) : [root]; return elements.flatMap((element) => { @@ -90,14 +95,18 @@ function unreadableTexts(locator: Locator, inside: boolean) { return ratio < min ? [`${ratio.toFixed(2)}:1 ${text.slice(0, 48)}`] : []; }); }, - { inside, min: MIN_CONTRAST }, + { inside, skip, min: MIN_CONTRAST }, ); } -async function expectReadable(locator: Locator, what: string, { inside = false } = {}) { +async function expectReadable( + locator: Locator, + what: string, + { inside = false, skip = null }: { inside?: boolean; skip?: string | null } = {}, +) { await expect(locator).toBeVisible(); await expect - .poll(() => unreadableTexts(locator, inside), { message: `${what} is readable` }) + .poll(() => unreadableTexts(locator, inside, skip), { message: `${what} is readable` }) .toEqual([]); } @@ -213,6 +222,9 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t await composer.fill("Draft a note about owls"); const sidebar = page.getByTestId("bots-sidebar"); const sidePanel = page.getByTestId("side-panel"); + await expect(sidebar.locator("[data-main-conversation]")).toBeVisible(); + await expect(sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" })).toBeVisible(); + await expect(page.getByTestId("trading-mode-status")).toBeVisible(); await inEachTheme(page, async (theme) => { await captureScreenshot(page, testInfo, `shell-contrast-${theme}`); @@ -220,27 +232,9 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t await expectReadable(page.getByTestId("composer-bar"), `${theme}: the composer`, { inside: true, }); - await expectReadable( - sidebar.getByRole("button", { name: "Heute", exact: true }), - `${theme}: Heute`, - { inside: true }, - ); - await expectReadable( - sidebar.locator("[data-main-conversation]"), - `${theme}: the main conversation row`, - { inside: true }, - ); - await expectReadable( - sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" }), - `${theme}: the selected bot row`, - { inside: true }, - ); - await expectReadable(page.getByTestId("trading-mode-status"), `${theme}: trading status`, { - inside: true, - }); - await expectReadable(page.getByTestId("user-menu-trigger"), `${theme}: the user menu`, { - inside: true, - }); + // The header carries the logo, which WCAG exempts; its create button is checked on its own. + await expectReadable(sidebar, `${theme}: the sidebar`, { inside: true, skip: ".app-drag" }); + await expectReadable(page.getByTestId("create-menu-trigger"), `${theme}: the create button`); }); await openNewBot(page); @@ -259,4 +253,12 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t await captureScreenshot(page, testInfo, `shell-contrast-${theme}-settings`); await expectReadable(sidePanel, `${theme}: the bot settings form`, { inside: true }); }); + + await sidebar.getByRole("button", { name: "Heute", exact: true }).click(); + const heute = page.getByTestId("heute-brief"); + await expect(heute.getByTestId("heute-mission-control-link")).toBeVisible(); + await inEachTheme(page, async (theme) => { + await captureScreenshot(page, testInfo, `shell-contrast-${theme}-heute`); + await expectReadable(heute, `${theme}: the Heute brief`, { inside: true }); + }); }); diff --git a/apps/web/src/components/DataTrustPanel.tsx b/apps/web/src/components/DataTrustPanel.tsx index 5400308..885fc91 100644 --- a/apps/web/src/components/DataTrustPanel.tsx +++ b/apps/web/src/components/DataTrustPanel.tsx @@ -10,16 +10,16 @@ const TRUST_ITEMS = [ export const DataTrustPanel = memo(function DataTrustPanel() { return (
-
- +
+ Datenschutz aktiv
{TRUST_ITEMS.map(({ icon: Icon, label, desc }) => ( -
- - {label} - · {desc} +
+ + {label} + · {desc}
))}
diff --git a/apps/web/src/components/TradingModeIndicator.tsx b/apps/web/src/components/TradingModeIndicator.tsx index 6780aa1..90c9622 100644 --- a/apps/web/src/components/TradingModeIndicator.tsx +++ b/apps/web/src/components/TradingModeIndicator.tsx @@ -45,7 +45,7 @@ export function TradingModeIndicator({ {presentation.subtitle ? ( - + ) : null} diff --git a/apps/web/src/pages/HeuteBrief.tsx b/apps/web/src/pages/HeuteBrief.tsx index cdf12b7..b6e9664 100644 --- a/apps/web/src/pages/HeuteBrief.tsx +++ b/apps/web/src/pages/HeuteBrief.tsx @@ -178,7 +178,7 @@ export function HeuteBrief({ href={MISSION_CONTROL_URL} target="_blank" rel="noreferrer" - className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground/70 transition-colors hover:text-muted-foreground" + className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground transition-colors hover:text-foreground" data-testid="heute-mission-control-link" > Mission Control @@ -235,7 +235,7 @@ function EmptyDay({ onStartChat, hasBots }: { onStartChat: () => void; hasBots: function ActionableChip({ label }: { label: string }) { return ( - + {label} ); diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index ae34218..f50bf3b 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -3456,7 +3456,7 @@ export function ShellPage() { className={`app-no-drag flex size-control-sm items-center justify-center rounded-full ${ activityMode ? "bg-primary text-primary-foreground" - : "text-muted-foreground/70 hover:text-foreground/75" + : "rk-sidebar-muted hover:text-foreground/75" }`} > @@ -3592,7 +3592,7 @@ export function ShellPage() { type="button" aria-label={t`Neue Unterhaltung`} data-testid="conversation-picker-trigger" - className="text-muted-foreground/60 hover:text-foreground/90 transition-colors" + className="rk-sidebar-muted hover:text-foreground/90 transition-colors" > @@ -3633,7 +3633,7 @@ export function ShellPage() {
) : null} {conversationSidebarGroups.length === 0 && !mainConversationEntry ? ( -
+

{t`Noch keine Unterhaltung.`}

{projectSidebarGroups.length === 0 ? ( -
+

{t`No projects yet.`}