From ac58521d95712a2b2475fffa2e9c6d5037434102 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:09:50 -0700 Subject: [PATCH 1/3] docs(selfhost): document the claude code token secret file + fix stale screenshots note Two drift fixes found while auditing the recent secrets-migration and screenshot-allowlist changes: - The security and AI-providers docs pages only described the inline .env path for CLAUDE_CODE_OAUTH_TOKEN; add the secrets/*.txt file alternative now that docker-compose.yml supports it. - config/examples/gittensory.full.yml and .gittensory.yml.example both still called `screenshots` a "plain symmetric override" -- #4990 made it allowlist-required (force-off only outside the rollout allowlist), the same asymmetry as safety/grounding. Update both mirrored comment blocks to match. --- .gittensory.yml.example | 18 +++++++++++------- .../routes/docs.self-hosting-ai-providers.tsx | 9 ++++++++- .../src/routes/docs.self-hosting-security.tsx | 12 ++++++------ config/examples/gittensory.full.yml | 18 +++++++++++------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/.gittensory.yml.example b/.gittensory.yml.example index 7047f10e57..f4257cb726 100644 --- a/.gittensory.yml.example +++ b/.gittensory.yml.example @@ -1106,13 +1106,17 @@ settings: # an operator who sets nothing keeps today's behavior). Exception: `safety` is force-on-only -- an untrusted # repo-controlled `false` is treated as "no opinion" rather than an active force-off (#2269), since a # lower-trust actor must never be able to silently defeat the operator's own security-hardening enablement. -# `e2eTests` (#4190), `screenshots` (#4616), and `improvementSignal` (#4738, foundation phase of the #4737 -# PR-improvement-signal epic) are plain symmetric overrides like rag/reputation/unifiedComment -- none carries -# a security-hardening or full-file-fetch rationale (unlike safety/grounding) that would justify a force-on/ -# force-off asymmetry. `screenshots` sits UNDER the separate, richer `review.visual.*` block further below -# (route/preview-URL config, and `review.visual.enabled: false` as an always-available additional force-off); -# this key only answers "does capture run for this repo at all," the same question its siblings answer for -# their own feature. `improvementSignal` is activation wiring only for now -- no tier reads the resolved +# `screenshots` (#4616) is ALSO asymmetric as of #4990: allowlisted is now a hard requirement for force-ON, +# the same as safety/grounding -- real Browserless rendering cost and a publicly-rendered PR image mean a +# lower-trust repo must not be able to self-activate capture just by setting this to `true` in its own +# config; a repo-level `false` can still force it OFF within an allowlisted repo. `screenshots` sits UNDER +# the separate, richer `review.visual.*` block further below (route/preview-URL config, and +# `review.visual.enabled: false` as an always-available additional force-off); this key only answers "does +# capture run for this repo at all," the same question its siblings answer for their own feature. +# `e2eTests` (#4190) and `improvementSignal` (#4738, foundation phase of the #4737 PR-improvement-signal +# epic) remain plain symmetric overrides like rag/reputation/unifiedComment -- neither carries a +# security-hardening or full-file-fetch rationale that would justify a force-on/force-off asymmetry. +# `improvementSignal` is activation wiring only for now -- no tier reads the resolved # value yet. # features: # rag: true diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx index 8711602164..80ffb75527 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx @@ -186,6 +186,13 @@ CLAUDE_AI_EFFORT=medium`} instead:

+

+ Prefer not pasting the raw token into .env? Write it into{" "} + secrets/claude_code_oauth_token.txt instead (see{" "} + Security's secret-file section) and leave{" "} + CLAUDE_CODE_OAUTH_TOKEN unset in .env — the same recreate step + above picks it up either way. +

Codex (subscription)

@@ -216,7 +223,7 @@ CLAUDE_AI_EFFORT=medium`} { title: "claude_code_no_oauth_token", description: - "CLAUDE_CODE_OAUTH_TOKEN is unset. Add it to .env and recreate the service.", + "CLAUDE_CODE_OAUTH_TOKEN is unset and no secrets/claude_code_oauth_token.txt file is populated. Set either and recreate the service.", }, { title: "claude_code_error_401", diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx index 83aeb69a4a..0a766f1b36 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-security.tsx @@ -56,12 +56,12 @@ function SelfHostingSecurity() { docker-compose.yml ships native Docker Compose secrets: mounts for the highest-value secrets (the GitHub App private key, webhook secret, API/MCP/internal-job tokens, the setup token, the two token-encryption master keys, the Orb enrollment secret, - and the PagerDuty routing key) — file-mounted at /run/secrets/<name>, - never exposed via docker inspect or docker compose config the way - a plain environment:/env_file value is. This is purely additive: - an inline .env value always takes priority if you set both, so you can migrate - one secret at a time, or not at all. See secrets/README.md for the full file - list. + the PagerDuty routing key, and the Claude Code subscription token) — file-mounted at{" "} + /run/secrets/<name>, never exposed via docker inspect or{" "} + docker compose config the way a plain environment:/ + env_file value is. This is purely additive: an inline .env value + always takes priority if you set both, so you can migrate one secret at a time, or not at + all. See secrets/README.md for the full file list.

Date: Sun, 12 Jul 2026 02:14:34 -0700 Subject: [PATCH 2/3] fix(ci): build gittensory-engine before UI typecheck on ui-only PR diffs validate-code's "Build engine package" step only ran on push, or when backend/engine changed -- not when only ui changed. But the very next "UI typecheck" step (gated separately on ui) transitively needs that build: apps/gittensory-ui's tsconfig pulls in src/mcp/local-write-tools.ts, which imports @jsonbored/gittensory-engine, whose dist/ is gitignored and only exists after this step runs. Any PR touching only apps/gittensory-ui/** (a docs-only page edit, for example) hit TS2307 here with no code being wrong -- confirmed live on PR #5201's own validate-code run. Add the missing `|| needs.changes.outputs.ui == 'true'` condition. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69d5c97575..fa8f20fd07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -282,7 +282,7 @@ jobs: # TS2307 in CI while passing locally against a stale leftover dist/ -- first hit by PR #5082). Now ahead # of both. - name: Build engine package - if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' }} + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }} run: npm run build --workspace @jsonbored/gittensory-engine # .tsbuildinfo mutates every run (tsc's own incremental state), unlike node_modules above which is # immutable per lockfile -- so this needs the run_id-suffixed-key + restore-keys-prefix pattern (always From 42b3f6a9ef8fcda7aa60814396a4f5dc223ff0de Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:22:12 -0700 Subject: [PATCH 3/3] fix(test): exercise the real lane-signal redaction path in ai-chat-qa #5149 added PRIVATE_LANE_SIGNAL_PATTERN and correctly wired it into redactGroundingText, applied to objective/summary/publicSafeSummary/ freshnessWarnings -- the only fields compactChatSignalBundle() ever reads. But its own new integration test overrode action.why instead, a field that function never reads at all (by design: raw action rationale/blockedBy is omitted entirely from the chat grounding bundle, not merely redacted -- see the comment above PRIVATE_DECISION_BLOCKER_PATTERN). The override never reached the prompt, so neither assertion exercised the new pattern; it happened to still report a false pass shape until CI caught the real failure downstream. Point the override at publicSafeSummary, the field that actually flows into the prompt, so the test validates what it claims to. No src/ change: redactGroundingText and its regex were already correct, proven by the adjacent pure-function unit test a few lines down in the same file, which was passing throughout. --- test/unit/ai-chat-qa.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/ai-chat-qa.test.ts b/test/unit/ai-chat-qa.test.ts index 2cb6be391e..f6c07fb3a0 100644 --- a/test/unit/ai-chat-qa.test.ts +++ b/test/unit/ai-chat-qa.test.ts @@ -261,13 +261,13 @@ describe("generateChatQaAnswer", () => { it("redacts private lane signals from cached rationale before prompting the provider", async () => { const run = vi.fn(async () => ({ response: "Public-safe readiness answer." })); const env = createTestEnv({ AI_ADVISORY: { run } as unknown as Ai, AI_DAILY_NEURON_BUDGET: "10000" }); + // publicSafeSummary (not why/blockedBy -- compactChatSignalBundle never reads either of those, by design; + // see the redaction-boundary comment above PRIVATE_DECISION_BLOCKER_PATTERN) is the field that actually + // reaches the prompt, so it's the one that must exercise the new PRIVATE_LANE_SIGNAL_PATTERN end-to-end. const result = await generateChatQaAnswer(env, { bundle: bundleFixture(undefined, { - why: [ - "owner/repo: Maintainer cut: 1.", - "owner/repo: split lane (direct PR 1, issue-discovery 1); both lanes are useful here.", - "owner/repo: direct PR lane share 1 with no hard personal blocker.", - ], + publicSafeSummary: + "Maintainer cut: 1. Split lane (direct PR 1, issue-discovery 1); both lanes are useful here. Direct PR lane share 1 with no hard personal blocker.", }), question: "what should I know?", advisoryAiRouting: ADVISORY_ON,