Skip to content

feat: two-tier token authentication (v6.0.0)#208

Merged
thebtf merged 12 commits into
mainfrom
worktree-pr-auth-v6
Apr 26, 2026
Merged

feat: two-tier token authentication (v6.0.0)#208
thebtf merged 12 commits into
mainfrom
worktree-pr-auth-v6

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Apr 26, 2026

Summary

Two-tier token authentication. Fixes the post-PR-203 regression (loom_*-only tool surface) by introducing a single auth.Validator shared by HTTP middleware and gRPC interceptor, and separating credentials into two host-pinned tiers.

This is a breaking change. Plugin minor: 5.2.5 → 6.0.0.

What changed

Layer Change
internal/auth/ NEW package — Validator, Identity, sentinel errors, context helpers. Two-strategy chain: master constant-time compare + bcrypt prefix lookup. 97% coverage.
internal/grpcserver/ authInterceptor and NEW streamAuthInterceptor delegate to auth.Validator. Streaming ProjectEvents now honours auth. Per-process token field replaced by *auth.Validator.
internal/worker/middleware.go HTTP middleware bearer arm delegates to auth.Validator. Removed inline authenticateClientToken (DRY). Cookie/forward-auth paths unchanged. NEW authSourceKey{} distinguishes session-cookie admins from bearer admins.
internal/worker/handlers_auth.go POST/GET/DELETE /api/auth/tokens (and …/stats) require admin session cookie. Bearer callers (operator key OR keycard) get 403 issuance requires browser session.
internal/handlers/engramcore/ Daemon reads ENGRAM_TOKEN (not ENGRAM_AUTH_ADMIN_TOKEN). connKey extended with tokenHash so distinct keycards never share a pooled gRPC conn (FR-7).
internal/handlers/serverevents/bridge.go Bridge reads ENGRAM_TOKEN.
cmd/engram/main.go NEW startupGate() — daemon exits 1 with single stderr line when ENGRAM_URL is set but ENGRAM_TOKEN is empty. Replaces silent graceful-degrade. daemonVersionv6.0.0.
internal/config/envnames.go NEW — single source of truth for env-var names.
Plugin .mcp.json env: ENGRAM_AUTH_ADMIN_TOKENENGRAM_TOKEN. Plugin version 6.0.0. userConfig.api_token.required: true, description rewritten. run-engram.js warns when operator key set on workstation. commands/setup.md walks user through dashboard issuance.
Docs README.md + docs/DEPLOYMENT.md + CHANGELOG.md document the two-tier model and migration steps.

No new external dependencies.

Spec / plan / clarifications

  • .agent/specs/auth-two-tier-tokens/spec.md (8 FR, 6 NFR, 7 US, 5 clarifications resolved)
  • .agent/specs/auth-two-tier-tokens/plan.md (6 phases, computed parallelism)
  • .agent/specs/auth-two-tier-tokens/architecture.md (10 ADRs, mermaid)
  • .agent/specs/auth-two-tier-tokens/user_job_statement.md (5 verbatim quotes)
  • .agent/specs/auth-two-tier-tokens/clarification-report-2026-04-26.md
  • .agent/specs/auth-two-tier-tokens/validation-report-2026-04-26.md

Migration

For users on plugin v5.x:

  1. /plugin update engram@engram (after this PR ships v6.0.0).
  2. Open <server-url>/tokens, log in as admin, generate a fresh keycard.
  3. Run /engram:setup, paste the keycard. Edit ~/.claude/settings.json to set ENGRAM_TOKEN and remove any leftover ENGRAM_AUTH_ADMIN_TOKEN / ENGRAM_API_TOKEN entries.
  4. Restart Claude Code. Daemon now exits loudly on misconfig.

Operator-side: leave ENGRAM_AUTH_ADMIN_TOKEN in the Docker compose .env. Server reads it; nothing else does.

Verification

  • go build ./... clean.
  • go test ./... green across all packages (auth coverage 97%, full suite passes).
  • go vet ./... clean for touched packages.

Test plan (post-merge, manual)

  • After v6.0.0 image rolls out: workstation upgrade plugin → fail-fast triggers (token absent yet).
  • Issue keycard via dashboard → paste via /engram:setup → restart CC.
  • Fresh CC session shows full tool surface (recall, vault, issues, feedback, docs, admin, loom_*).
  • Revoke keycard via dashboard → next gRPC call within 65 s rejects → daemon reconnect loop visible.
  • POST /api/auth/tokens with bearer (any tier) → 403. With session cookie + admin role → 200.

Risks / known gaps

  • U1 (P2 task 5): server-side ProjectEvents per-event re-validation hook is documented in the spec but not landed in this PR's diff. The streaming interceptor validates at stream open; mid-stream revocation latency is bounded by the heartbeat reconnect cycle (~60 s) rather than per-event. This matches Clarification C3's intent; tracked as a follow-up if the user finds the bound too loose.
  • U3 (Plan): the pre-existing Release (goreleaser) job has been failing on ui/package.json.tpl staleness since v5.2.5. Tagging v6.0.0 will repeat the failure unless .tpl is regenerated first. Not part of this spec; flagged for the release runner.

Refs

Closes the PR-203 regression. Full feature spec: .agent/specs/auth-two-tier-tokens/spec.md.

Summary by CodeRabbit

  • Критические изменения

    • Введена двухуровневая модель аутентификации: рабочие станции обязаны использовать per‑workstation токены (ENGRAM_TOKEN); операторский админ‑ключ — только для сервера.
    • Нет fallback между переменными: демон при заданном ENGRAM_URL и пустом ENGRAM_TOKEN завершится с понятным сообщением в stderr.
    • Выдача/ревокация токенов теперь требует админ‑сессионного входа в браузере; bearer‑запросы для этих операций получают 403.
    • Плагин/CLI теперь требуют per‑workstation API‑token (поле стало обязательным).
  • Документация

    • README, DEPLOYMENT, setup‑гайд, CHANGELOG и playbook обновлены; добавлены миграционные инструкции и рекомендации по выдаче/ротации токенов.

thebtf added 5 commits April 26, 2026 17:46
Phase 1 of auth-two-tier-tokens (v6.0.0):

- internal/auth/identity.go — Identity value type + Source enum (master/client/session)
- internal/auth/errors.go — sentinel errors (ErrEmptyToken, ErrInvalidCredentials, ErrRevoked)
- internal/auth/validator.go — Validator with two-arm chain (master compare + bcrypt prefix lookup)
- internal/auth/context.go — RoleKey/SourceKey context helpers + IsSessionAdmin gate
- internal/auth/validator_test.go — table-driven tests covering every branch
- internal/auth/context_test.go — context round-trip + IsSessionAdmin discrimination

Coverage 97.5%; go vet clean. No external deps added.

Refs: spec.md FR-1, FR-5; plan.md Phase 1 (T1.1..T1.5).
Phase 2 of auth-two-tier-tokens (v6.0.0):

- internal/grpcserver/server.go:
  * drop ENV-derived `token` field; replace with `*auth.Validator`
  * add SetValidator() setter (mirrors SetDB/SetBus pattern)
  * authInterceptor delegates to validator.Validate, maps to gRPC codes
  * NEW streamAuthInterceptor for ProjectEvents stream method
  * NEW authedStream wrapper exposes auth.Identity via Context()
- internal/grpcserver/sync_project_state.go: withDB() copies validator field
- internal/grpcserver/auth_interceptor_test.go (NEW): direct interceptor
  unit tests covering Ping skip, missing metadata, missing header,
  master-token success, valid keycard success, invalid token, raw bearer
  without prefix, streaming-valid, streaming-missing, streaming-revoked.
- internal/auth/context.go: simplify to single IdentityKey (full Identity
  in context, including KeycardID for audit/stats handlers)
- internal/auth/context_test.go: tests adjusted to new key
- internal/worker/service.go: build *auth.Validator before grpcserver.New;
  pass nil when ENGRAM_AUTH_DISABLED=true (mirrors prior token=="" branch).

Build clean; auth pkg coverage 97.1%; grpcserver tests green.

Refs: spec.md FR-2, FR-6, US1, US5; plan.md Phase 2 (T2.1..T2.6).
…hardened

Phase 3 of auth-two-tier-tokens (v6.0.0):

- internal/worker/middleware.go:
  * TokenAuth gains validator field + SetValidator() setter
  * Middleware bearer/X-Auth-Token/SSE-query path delegates to validator
  * Removed inline authenticateClientToken (DRY: single chain shared with gRPC)
  * Read-only scope gate + stats increment now key off auth.Identity
  * NEW emptyTokenStore for bootstrap window before SetValidator() swap
  * NEW extractHTTPBearer / isMutatingMethod / buildAuthCtx helpers
  * buildAuthCtx writes BOTH auth.IdentityKey and legacy authRoleKey for
    backward compat with handlers that still read worker.authRoleKey{}

- internal/worker/handlers_auth.go:
  * NEW requireSessionAdmin guard: 403 when caller is not session-cookie admin
  * Applied to handleCreateToken, handleListTokens, handleRevokeToken,
    handleGetTokenStats — issuance is browser-only per FR-6 / Clarification C4
  * Bearer-token paths (master OR worker keycard) explicitly rejected on
    these endpoints with deterministic JSON 403 body

- internal/worker/service.go:
  * Wire shared *auth.Validator into BOTH gRPC server AND HTTP middleware
    after tokenStore is ready (FR-2: symmetric validation on both transports)

All worker tests green. Build + vet clean.

Refs: spec.md FR-2, FR-6, US2, US4-bis; plan.md Phase 3 (T3.1..T3.5).
…st, pool keys credential

Phase 4 of auth-two-tier-tokens (v6.0.0):

- internal/config/envnames.go (NEW): single source of truth for the four
  canonical env vars (EnvServerURL, EnvServerURLAlt, EnvAdminToken,
  EnvWorkstationToken). Renames are now caught at compile time.

- internal/handlers/engramcore/tools.go: ProxyTools + ProxyHandleTool read
  config.EnvWorkstationToken (= "ENGRAM_TOKEN"). The historical
  ENGRAM_AUTH_ADMIN_TOKEN is intentionally NOT consulted here (FR-3 + FR-5).

- internal/handlers/engramcore/grpcpool.go: connKey gains a tokenHash field
  (sha256-of-token, first 16 hex chars). Two sessions on the same workstation
  with distinct keycards no longer share a pooled connection (FR-7 / ADR-005).
  Empty token -> empty hash, allowing the no-auth degenerate case for tests.

- internal/handlers/serverevents/bridge.go: NewBridge reads
  config.EnvWorkstationToken. The pre-v6 admin-token name is dropped here
  too (FR-5: no legacy fallback chains).

- cmd/engram/main.go: NEW startupGate() runs before any module init. With a
  configured server URL but empty ENGRAM_TOKEN -> single stderr line +
  os.Exit(1) (FR-4 / ADR-005). With no server URL configured the gate is
  silent, allowing local-only loom_* flows. daemonVersion bumped to v6.0.0.

Build + tests green across auth / grpcserver / worker / engramcore /
serverevents / config packages.

Refs: spec.md FR-3, FR-4, FR-5, FR-7, US3; plan.md Phase 4 (T4.1..T4.6).
Phase 5 of auth-two-tier-tokens (v6.0.0):

- plugin/engram/.mcp.json: env key ENGRAM_AUTH_ADMIN_TOKEN -> ENGRAM_TOKEN.
- plugin/engram/.claude-plugin/plugin.json: version 5.2.5 -> 6.0.0.
  Description leads with BREAKING change banner. userConfig.api_token
  description rewritten to make the dashboard /tokens issuance flow
  explicit and to forbid pasting the operator key. api_token now required.
- plugin/engram/scripts/run-engram.js: warning text points users to
  <serverURL>/tokens for keycard issuance. Active warning when
  ENGRAM_AUTH_ADMIN_TOKEN is set on a workstation (forbidden in v6).
- plugin/engram/commands/setup.md: full v6 walkthrough — issue keycard via
  dashboard, paste, restart. Refuses operator-key paste at validation step.
- README.md: NEW "Two-Tier Token Model" section in What's New;
  v6.0.0 highlight added.
- docs/DEPLOYMENT.md: NEW Token Model section explaining the host-pinned
  separation between ENGRAM_AUTH_ADMIN_TOKEN (server-host only) and
  ENGRAM_TOKEN (workstation only).
- CHANGELOG.md: NEW v6.0.0 entry with BREAKING CHANGES, migration steps,
  Added/Removed sections.

Build + tests green. Phase 5 closes the v6 spec implementation.

Refs: spec.md FR-3, FR-8, US1, US2; plan.md Phase 5 (T5.1..T5.8).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Выпущен v6.0.0: введена двухуровневая модель токенов (server admin vs per‑workstation keycards), общий auth.Validator с единым поведением для HTTP и gRPC, контекстная Identity, fail‑fast стартап при заданном ENGRAM_URL и пустом ENGRAM_TOKEN, плюс соответствующие изменения документации и плагина.

Changes

Cohort / File(s) Summary
Релиз и документация
CHANGELOG.md, README.md, docs/DEPLOYMENT.md, docs/PRODUCTION-TESTING-PLAYBOOK.md, plugin/engram/commands/setup.md
Добавлены заметки v6.0.0: BREAKING двухуровневая модель токенов, миграция, поведение fail‑fast при ENGRAM_URL + пустом ENGRAM_TOKEN; обновлены setup‑инструкции и playbook.
CLI / startup
cmd/engram/main.go, plugin/engram/scripts/run-engram.js
Добавлен startupGate() — демоны завершаются с ошибкой, если ENGRAM_URL задан и ENGRAM_TOKEN пуст; bumped daemon версии до v6.0.0; run wrapper обновлён для новых env.
Plugin / MCP конфиг
plugin/engram/.claude-plugin/plugin.json, plugin/engram/.mcp.json
Плагин повышен до 6.0.0; api_token помечен как обязательный per‑workstation keycard; env‑переменная в MCP → ENGRAM_TOKEN.
Core auth модель
internal/auth/identity.go, internal/auth/context.go, internal/auth/errors.go
Новая Identity модель (Role/Source/KeycardID), контекстные helper‑ы, sentinel ошибки (ErrEmptyToken, ErrInvalidCredentials, ErrRevoked).
Validator и тесты
internal/auth/validator.go, internal/auth/validator_test.go, tests/critical/auth_two_tier_test.go
Новый Validator (master vs client flow), shape‑gate engram_, TokenStoreReader интерфейс; покрытие unit + e2e тестом, включая bufconn gRPC и HTTP.
Константы окружения
internal/config/envnames.go
Централизованы canonical имена env‑переменных: EnvServerURL, EnvServerURLAlt, EnvAdminToken, EnvWorkstationToken.
gRPC сервер и интерсепторы
internal/grpcserver/server.go, internal/grpcserver/auth_interceptor_test.go, internal/grpcserver/sync_project_state.go
Server теперь хранит *auth.Validator (mu‑guard), интерсепторы используют Validator, добавлен SetValidator, ошибки маппятся в gRPC коды; тесты обновлены.
gRPC пул и соединения
internal/handlers/engramcore/grpcpool.go
Pool key расширен: (grpcAddr, tlsMode, tokenHash) — хеш токена препятствует шарингу ClientConn между разными токенами.
Handlers и middleware HTTP
internal/worker/middleware.go, internal/worker/handlers_auth.go, internal/worker/service.go
HTTP middleware переведён на shared auth.Validator, добавлен SetValidator, requireSessionAdmin и read‑only gating для client keycards; инициализация сервиса строит общий Validator и прокачивает его в gRPC/HTTP.
Handlers engramcore / bridge / tools
internal/handlers/engramcore/tools.go, internal/handlers/serverevents/bridge.go
Заменены хардкод‑env на config константы (EnvWorkstationToken, EnvServerURL*) и соответствующие комментарии.
UI / сборка
ui/package.json.tpl
Обновлён набор зависимостей frontend (нефункциональные для auth изменения).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant "gRPC Server"
    participant Validator
    participant "Token Store"

    Client->>"gRPC Server": RPC с Bearer Token (metadata)
    "gRPC Server"->>"gRPC Server": extractBearer(token)
    alt token == ""
        "gRPC Server"-->>Client: Unauthenticated (ErrEmptyToken)
    else
        "gRPC Server"->>Validator: Validate(ctx, token)
        alt master token matches
            Validator-->>"gRPC Server": Identity(RoleAdmin, SourceMaster)
        else
            Validator->>Validator: check shape/prefix (engram_)
            Validator->>"Token Store": FindByPrefix(prefix)
            "Token Store"-->>Validator: candidates[]
            alt bcrypt match найден
                Validator-->>"gRPC Server": Identity(RoleX, SourceClient, KeycardID)
            else
                Validator-->>"gRPC Server": ErrInvalidCredentials
                "gRPC Server"-->>Client: Unauthenticated ("invalid token")
            end
        end
        alt auth success
            "gRPC Server"->>"gRPC Server": WithIdentity(ctx, identity)
            "gRPC Server"-->>Client: Выполнение хендлера / ответ
        end
    end
Loading

Estimated code review effort

🎯 4 (Сложно) | ⏱️ ~60 минут

Possibly related PRs

Poem

🐰 Токен разделён — два яруса в деле,
Админ живёт в сервере, рабочий в кармане смело.
Валидатор шнурует шифры, префикс глядит зорко,
Контекст шлёт личность в вызов — и всё идёт чётко,
Старт просит токен: без него — уходи в покой!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: two-tier token authentication (v6.0.0)' accurately and clearly summarizes the main change: implementation of two-tier token authentication for v6.0.0.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-pr-auth-v6

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a two-tier token authentication model (v6.0.0), distinguishing between server-side operator keys and workstation-specific keycards. It introduces a centralized internal/auth package to unify validation across HTTP and gRPC, adds a fail-fast startup check for missing credentials, and updates the plugin and documentation to reflect these breaking changes. Review feedback identifies unreachable error handling for revoked tokens in the validator and suggests ensuring consistent JSON content-types in authentication error responses.

Comment thread internal/auth/validator.go
Comment thread internal/worker/handlers_auth.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
README.md (2)

175-177: ⚠️ Potential issue | 🟡 Minor

Несоответствие: Quick Start использует устаревший ENGRAM_AUTH_ADMIN_TOKEN для клиента.

Согласно v6 модели, рабочие станции должны использовать ENGRAM_TOKEN. Строка 176 показывает ENGRAM_AUTH_ADMIN_TOKEN, что противоречит документации в строках 43-48 этого же файла.

Предлагаемое исправление
 # Linux/macOS: add to shell profile
 # Windows: set as System Environment Variables
 ENGRAM_URL=http://your-server:37777/mcp
-ENGRAM_AUTH_ADMIN_TOKEN=your-admin-token
+ENGRAM_TOKEN=your-worker-keycard
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 175 - 177, The Quick Start env example uses the
outdated variable ENGRAM_AUTH_ADMIN_TOKEN; update the snippet to use
ENGRAM_TOKEN instead so it matches the v6 model and the other documentation in
this file—replace ENGRAM_AUTH_ADMIN_TOKEN with ENGRAM_TOKEN in the example and
ensure any accompanying text or examples referencing ENGRAM_AUTH_ADMIN_TOKEN are
updated to ENGRAM_TOKEN for consistency.

193-194: ⚠️ Potential issue | 🟡 Minor

Ещё одна ссылка на устаревший ENGRAM_AUTH_ADMIN_TOKEN в разделе Installation.

Аналогично Quick Start, здесь также используется admin token вместо worker keycard.

Предлагаемое исправление
 # Set environment variables first
 ENGRAM_URL=http://your-server:37777/mcp
-ENGRAM_AUTH_ADMIN_TOKEN=your-admin-token
+ENGRAM_TOKEN=your-worker-keycard
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 193 - 194, В разделе Installation замените устаревшую
переменную окружения ENGRAM_AUTH_ADMIN_TOKEN на переменную для worker keycard
(например ENGRAM_AUTH_WORKER_KEYCARD) и обновите пример значения соответственно
(worker keycard вместо "your-admin-token"); найдите и исправьте строку с
ENGRAM_URL/ENGRAM_AUTH_ADMIN_TOKEN и приведите её к виду
ENGRAM_URL=http://your-server:37777/mcp и
ENGRAM_AUTH_WORKER_KEYCARD=your-worker-keycard, чтобы соответствовать
использованию worker keycard как в Quick Start.
docs/DEPLOYMENT.md (1)

139-147: ⚠️ Potential issue | 🟡 Minor

Несоответствие: устаревшие ссылки на ENGRAM_API_TOKEN.

В разделе "Client Setup" используется ENGRAM_API_TOKEN (строки 141, 146), который был удалён в v6. Согласно CHANGELOG.md и новой модели токенов, клиенты должны использовать ENGRAM_TOKEN.

Предлагаемое исправление
   **macOS / Linux** (`~/.bashrc` or `~/.zshrc`):
   ```bash
   export ENGRAM_URL=http://your-server:37777/mcp
-  export ENGRAM_API_TOKEN=your-secret-token
+  export ENGRAM_TOKEN=your-worker-keycard-token

Windows (PowerShell as admin):

[Environment]::SetEnvironmentVariable("ENGRAM_URL", "http://your-server:37777/mcp", "User")
-  [Environment]::SetEnvironmentVariable("ENGRAM_API_TOKEN", "your-secret-token", "User")
+  [Environment]::SetEnvironmentVariable("ENGRAM_TOKEN", "your-worker-keycard-token", "User")
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/DEPLOYMENT.md around lines 139 - 147, Update the "Client Setup"
examples to use the new token name: replace any occurrence of ENGRAM_API_TOKEN
with ENGRAM_TOKEN and change the example value from "your-secret-token" to
"your-worker-keycard-token"; ensure both the bash export line and the PowerShell
[Environment]::SetEnvironmentVariable call reflect ENGRAM_TOKEN and the new
example value so documentation matches the v6 token model.


</details>

</blockquote></details>

</blockquote></details>
🧹 Nitpick comments (1)
internal/auth/context_test.go (1)

18-21: Проверьте в round-trip ещё и KeycardID.

Сейчас тест фиксирует только Role и Source, поэтому регрессия в переносе KeycardID через WithIdentity/IdentityFrom останется незамеченной.

Минимальное усиление теста
 assert.True(t, ok)
 assert.Equal(t, auth.RoleReadWrite, got.Role)
 assert.Equal(t, auth.SourceClient, got.Source)
+assert.Equal(t, "uuid-1", got.KeycardID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/context_test.go` around lines 18 - 21, Тест round-trip сейчас
проверяет только Role и Source, добавьте проверку на корректную передачу
KeycardID через WithIdentity/IdentityFrom: when building the identity in the
test (the code that calls WithIdentity) set a known KeycardID and then assert
that got.KeycardID equals that expected value (alongside existing assertions for
auth.RoleReadWrite and auth.SourceClient) so regressions in KeycardID
propagation are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 18-19: В CHANGELOG.md найдена ссылка на несуществующий файл
спецификации `.agent/specs/auth-two-tier-tokens/spec.md`; исправьте это либо
создав требуемую спецификацию по этому пути с содержимым spec.md, либо
удалив/заменив ссылку в CHANGELOG.md; при создании файла убедитесь, что
директории `.agent/specs/auth-two-tier-tokens/` существуют и файл называется
spec.md, при удалении — обновите сопроводительный текст в CHANGELOG.md чтобы он
корректно отражал изменения.

In `@internal/auth/validator.go`:
- Around line 123-129: При проверке токена не доверяй свободному тексту в
candidates[i].Scope: вместо прямого возврата Client(candidates[i].Scope,
candidates[i].ID) принимай только заранее определённые значения scope (например
"read", "write", "service" или список, используемый в IsAdmin/role-checks); если
scope не в белом списке — возвращай ошибку валидации. Изменения: в том месте,
где bcrypt совпал (в цикле по candidates и перед вызовом Client), проверяй
candidates[i].Scope против констант/перечисления и только при совпадении строь
Identity через Client, иначе возвращай явную ошибку данных.

In `@internal/grpcserver/server.go`:
- Around line 83-89: SetValidator currently writes to s.validator without
synchronization causing data races because interceptors read s.validator on
request goroutines; make access safe by replacing the plain field with a
concurrency-safe holder (e.g. atomic.Value) or guarding it with a mutex: change
Server.validator to hold an atomic.Value (storing *auth.Validator) or add a
sync.RWMutex, update SetValidator to call validatorHolder.Store(v) (or
lock+assign) and update all interceptor/read sites to use validatorHolder.Load()
(or RLock()+read) and nil-check before calling s.validator.Validate(...). Ensure
the readers never assume non-nil and handle nil validator gracefully.

In `@internal/handlers/engramcore/grpcpool.go`:
- Around line 21-33: The connKey struct must include the ENGRAM_TLS_CA identity
so connections built with different CA configs aren’t reused; add a new field
(e.g. tlsCAPath or tlsCAHash) to connKey and ensure getOrDialGRPC (and any pool
lookups/constructors that create connKey) populates that field (compute a short
hash of the ENGRAM_TLS_CA path/value if you want constant length) so the key
distinguishes "custom-ca" variants; update any places that compare or construct
connKey (and related comments) so the CA value/hash is part of the pool lookup.

In `@internal/handlers/serverevents/bridge.go`:
- Around line 108-118: The current env-var precedence in bridge.go is inverted
compared to the rest of the daemon: change the server URL lookup to prefer
config.EnvServerURL (ENGRAM_SERVER_URL) first and only fall back to
config.EnvServerURLAlt (ENGRAM_URL), keeping the existing deprecation
logger.Warn message when the fallback is used; update the serverURL assignment
logic that defines the serverURL variable (used by NewBridge/bridge
initialization) to reflect that order so tools/call and serverevents bridge use
the same backend.

In `@internal/worker/handlers_auth.go`:
- Around line 291-301: The 403 branch in requireSessionAdmin currently uses
http.Error which forces text/plain and adds a newline, so replace that with a
proper JSON response: set Content-Type: application/json on the ResponseWriter,
write the 403 status with w.WriteHeader, and encode the error body as a JSON
object (e.g., {"error":"issuance requires browser session — log in to the
dashboard at /login"}) using json.NewEncoder or equivalent; update only the 403
branch in requireSessionAdmin to ensure consistent JSON responses across the
API.

In `@internal/worker/middleware.go`:
- Around line 383-388: The auto-provisioning creates a user with role "operator"
which authpkg.Session treats as non-admin, causing IsAdmin/IsSessionAdmin checks
to fail; change the provisioning so uStore.CreateUser(authentikEmail, "",
"operator") creates the user with an admin role (e.g., "admin"), or ensure
authpkg.Session(...) returns an admin session when authentikAutoProvision is
true—update the call in the authentikAutoProvision block (around
uStore.CreateUser and authpkg.Session) to produce an admin session for the newly
created user so session-only endpoints work.
- Around line 320-327: Validator.Validate возвращает и аутентификационные
ошибки, и внутренние ошибки уровня хранилища/bcrypt, поэтому нельзя всегда
отвечать 401; поменяй обработку результата validator.Validate в middleware.go
так, чтобы сначала отличать внутренние ошибки (используя errors.Is/вложенные
ошибки от store/bcrypt или конкретные sentinel errors, например ошибки из пакета
store/bcrypt, и т.д.) и для них логировать и отвечать Internal Server Error
(500), а для реальных ошибок авторизации — возвращать 401; обнови вызов log и
http.Error соответственно (оставить log.Warn/Err для 401, а для внутренних
ошибок использовать log.Error и 500), и не забудь добавить импорт "errors".

---

Outside diff comments:
In `@docs/DEPLOYMENT.md`:
- Around line 139-147: Update the "Client Setup" examples to use the new token
name: replace any occurrence of ENGRAM_API_TOKEN with ENGRAM_TOKEN and change
the example value from "your-secret-token" to "your-worker-keycard-token";
ensure both the bash export line and the PowerShell
[Environment]::SetEnvironmentVariable call reflect ENGRAM_TOKEN and the new
example value so documentation matches the v6 token model.

In `@README.md`:
- Around line 175-177: The Quick Start env example uses the outdated variable
ENGRAM_AUTH_ADMIN_TOKEN; update the snippet to use ENGRAM_TOKEN instead so it
matches the v6 model and the other documentation in this file—replace
ENGRAM_AUTH_ADMIN_TOKEN with ENGRAM_TOKEN in the example and ensure any
accompanying text or examples referencing ENGRAM_AUTH_ADMIN_TOKEN are updated to
ENGRAM_TOKEN for consistency.
- Around line 193-194: В разделе Installation замените устаревшую переменную
окружения ENGRAM_AUTH_ADMIN_TOKEN на переменную для worker keycard (например
ENGRAM_AUTH_WORKER_KEYCARD) и обновите пример значения соответственно (worker
keycard вместо "your-admin-token"); найдите и исправьте строку с
ENGRAM_URL/ENGRAM_AUTH_ADMIN_TOKEN и приведите её к виду
ENGRAM_URL=http://your-server:37777/mcp и
ENGRAM_AUTH_WORKER_KEYCARD=your-worker-keycard, чтобы соответствовать
использованию worker keycard как в Quick Start.

---

Nitpick comments:
In `@internal/auth/context_test.go`:
- Around line 18-21: Тест round-trip сейчас проверяет только Role и Source,
добавьте проверку на корректную передачу KeycardID через
WithIdentity/IdentityFrom: when building the identity in the test (the code that
calls WithIdentity) set a known KeycardID and then assert that got.KeycardID
equals that expected value (alongside existing assertions for auth.RoleReadWrite
and auth.SourceClient) so regressions in KeycardID propagation are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d19a73f8-722d-4b56-a24c-a3eefb70d81e

📥 Commits

Reviewing files that changed from the base of the PR and between 187a2e7 and 2feb662.

📒 Files selected for processing (24)
  • CHANGELOG.md
  • README.md
  • cmd/engram/main.go
  • docs/DEPLOYMENT.md
  • internal/auth/context.go
  • internal/auth/context_test.go
  • internal/auth/errors.go
  • internal/auth/identity.go
  • internal/auth/validator.go
  • internal/auth/validator_test.go
  • internal/config/envnames.go
  • internal/grpcserver/auth_interceptor_test.go
  • internal/grpcserver/server.go
  • internal/grpcserver/sync_project_state.go
  • internal/handlers/engramcore/grpcpool.go
  • internal/handlers/engramcore/tools.go
  • internal/handlers/serverevents/bridge.go
  • internal/worker/handlers_auth.go
  • internal/worker/middleware.go
  • internal/worker/service.go
  • plugin/engram/.claude-plugin/plugin.json
  • plugin/engram/.mcp.json
  • plugin/engram/commands/setup.md
  • plugin/engram/scripts/run-engram.js

Comment thread CHANGELOG.md Outdated
Comment thread internal/auth/validator.go Outdated
Comment thread internal/grpcserver/server.go
Comment thread internal/handlers/engramcore/grpcpool.go Outdated
Comment thread internal/handlers/serverevents/bridge.go Outdated
Comment thread internal/worker/handlers_auth.go Outdated
Comment thread internal/worker/middleware.go Outdated
Comment thread internal/worker/middleware.go
thebtf added 2 commits April 26, 2026 20:12
The .tpl was last updated for the pre-shadcn UI; recent SystemView /
IssueDetailView / TokensView / VaultView refactor added 12+ deps to
ui/package.json (reka-ui, lucide-vue-next, marked, dompurify, shiki,
@shikijs/transformers, @vueuse/core, clsx, tailwind-merge,
class-variance-authority, tailwindcss-animate, vue-sonner, font packages)
that never made it into the .tpl.

goreleaser regenerates ui/package.json from .tpl in its before-hook, then
runs npm install + vue-tsc — vue-tsc fails with TS2307 across ~30 files
because the deps are missing. The Release CI job has been red since v5.2.5.
This is the pre-existing block flagged in plan.md U3 — fixing it here so
the v6.0.0 tag does not inherit the failure.

Docker CI (which uses ui/package.json directly) is unaffected.
MEDIUM-1 (data race): grpcserver.Server gains sync.RWMutex; SetValidator
locks for swap, validateBearer reads through currentValidator() under
RLock. Matches TokenAuth.mu pattern.

MEDIUM-2 (duplicate constants): export TokenRawPrefix / TokenPrefixLen /
TokenMinLen from internal/auth as the single source of truth.
internal/worker/handlers_auth.go now aliases them locally — editing the
literal values is now a contract change at the auth package, not a
silent drift between producer and consumer.

MEDIUM-3 (Content-Type): requireSessionAdmin emits proper
application/json + json-encoded body for 403 responses (was text/plain
with JSON-shaped string).

LOW-1: serverevents/bridge.go token field comment was stale
(ENGRAM_AUTH_ADMIN_TOKEN). Fixed to ENGRAM_TOKEN.

LOW-2: middleware.go startup warn used pre-v5 ENGRAM_API_TOKEN env name.
Updated to ENGRAM_AUTH_ADMIN_TOKEN.

LOW-3: validateBearer ErrRevoked branch documented as currently
unreachable (FindByPrefix filters revoked at SQL layer) but kept as the
explicit mapping for future contract changes.

LOW-4: NEW test TestValidate_EmptyMaster_KeycardStillWorks covers the
documented "Tier-2-only" mode (masterToken == "", valid keycard).

Build + tests green across all touched packages.

Refs: PR #208 local code review (nvmd-platform:code-reviewer subagent).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/worker/handlers_auth.go (1)

313-325: ⚠️ Potential issue | 🟡 Minor

Swagger-описание auth-gate уже расходится с фактическими ответами.

requireSessionAdmin() теперь добавляет для этого хендлера 401 и JSON-объект на 403, но аннотация всё ещё описывает только string-403 и вообще не документирует 401. Из-за этого сгенерированные клиенты и docs будут ожидать неверный контракт; тот же дрейф теперь есть и у list/revoke/stats, которые вызывают этот же guard.

Минимальное обновление аннотаций
 // `@Success` 200 {object} map[string]interface{}
 // `@Failure` 400 {string} string "bad request"
-// `@Failure` 403 {string} string "forbidden — issuance requires browser session"
+// `@Failure` 401 {string} string "unauthorized"
+// `@Failure` 403 {object} map[string]string "forbidden — issuance requires browser session"
 // `@Failure` 409 {string} string "conflict"
 // `@Failure` 500 {string} string "internal error"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_auth.go` around lines 313 - 325, Swagger annotations
for handleCreateToken (and the other handlers protected by requireSessionAdmin
such as the list/revoke/stats handlers) are out of sync: requireSessionAdmin now
can return a 401 and returns JSON for 403 rather than a plain string. Update the
godoc comments for handleCreateToken and the list/revoke/stats handlers to add a
`@Failure` 401 entry that documents a JSON error response (e.g. {object}
map[string]interface{} or your shared ErrorResponse type) and change the
existing `@Failure` 403 to document a JSON object (not string); ensure the
documented response schema matches the actual error payload your auth guard
returns.
♻️ Duplicate comments (2)
internal/handlers/serverevents/bridge.go (1)

108-113: ⚠️ Potential issue | 🟠 Major

Нужно выровнять приоритет URL-переменных с канонической схемой.

Сейчас на Line 108 bridge сначала читает EnvServerURLAlt (ENGRAM_SERVER_URL), а только потом на Line 110EnvServerURL (ENGRAM_URL). Это расходится с internal/config/envnames.go и startupGate() в cmd/engram/main.go, где канонический приоритет обратный. При одновременной установке обеих переменных процесс может пойти в разные backend’ы в разных подсистемах.

💡 Предлагаемое исправление
-	serverURL := os.Getenv(config.EnvServerURLAlt)
+	serverURL := os.Getenv(config.EnvServerURL)
 	if serverURL == "" {
-		if fallback := os.Getenv(config.EnvServerURL); fallback != "" {
+		if fallback := os.Getenv(config.EnvServerURLAlt); fallback != "" {
 			serverURL = fallback
-			logger.Warn("ENGRAM_URL is deprecated for bridge configuration; please use ENGRAM_SERVER_URL instead")
+			logger.Warn("ENGRAM_SERVER_URL is deprecated for bridge configuration; please use ENGRAM_URL instead")
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/serverevents/bridge.go` around lines 108 - 113, The bridge
currently reads EnvServerURLAlt into serverURL first and only then falls back to
EnvServerURL, which reverses the canonical priority; change the logic in
bridge.go so serverURL is populated from config.EnvServerURL (ENGRAM_URL) first
and only if empty use config.EnvServerURLAlt (ENGRAM_SERVER_URL), and update the
logger.Warn message to reflect the correct deprecated/primary variable names;
refer to the serverURL variable and the EnvServerURL/EnvServerURLAlt symbols
when making this change.
internal/auth/validator.go (1)

127-133: ⚠️ Potential issue | 🟠 Major

Не доверяйте api_tokens.scope как готовой runtime-роли.

Client(candidates[i].Scope, ...) всё ещё превращает произвольный text из БД в Identity.Role. Значение вроде "admin" сразу начнёт проходить через Identity.IsAdmin(), так что повреждённые или вручную вставленные строки в api_tokens.scope дают эскалацию прав.

Минимальная защита на месте чтения
 		if err == nil {
-			return Client(candidates[i].Scope, candidates[i].ID), nil
+			switch Role(candidates[i].Scope) {
+			case RoleReadWrite, RoleReadOnly:
+				return Client(candidates[i].Scope, candidates[i].ID), nil
+			default:
+				return Identity{}, fmt.Errorf("auth: unexpected token scope %q", candidates[i].Scope)
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/validator.go` around lines 127 - 133, Кратко: текущая строка
Client(candidates[i].Scope, candidates[i].ID) напрямую превращает сырой текст из
api_tokens.scope в роль и позволяет эскалацию; нужно заменить это на безопасную
валидацию/маппинг строки в заранее определённый набор ролей. Исправьте код в
месте сравнения токена (в блоке, где используется candidates и вызывается
Client): вместо прямого приведения строки реализуйте/вызовите функцию вроде
parseRole(scope string) или mapScopeToRole(scope string) которая проверяет
значение против белого списка допустимых ролей (например "user","admin",...) и
возвращает конкретный тип/enum Role или ошибку/дефолтную роль; при неизвестном
или недопустимом значении отказывайте в аутентификации или назначайте
минимальные права, не позволяя произвольной строке пройти в Identity.Role.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/auth/validator.go`:
- Around line 113-118: Tighten the "shape gate" in the validator so it only
accepts the exact token format (TokenRawPrefix followed by 32 hex characters)
before calling FindByPrefix: in the function handling raw tokens, replace the
current prefix/length checks (raw, TokenRawPrefix, TokenMinLen, TokenPrefixLen)
with a strict validation that raw == TokenRawPrefix + 32 chars and that those 32
chars are valid hex; if validation fails return ErrInvalidCredentials to
fail-closed and avoid unnecessary FindByPrefix/DB/bcrypt work.

In `@internal/grpcserver/server.go`:
- Around line 62-78: The issue: interceptors are only added in New() when
validator != nil so calling SetValidator(v) later won't enable auth; fix by
always registering grpc.UnaryInterceptor(srv.authInterceptor) and
grpc.StreamInterceptor(srv.streamAuthInterceptor) in New() regardless of
validator value and make authInterceptor and streamAuthInterceptor check
srv.validator at runtime to bypass when nil, or alternatively modify
SetValidator to reject changing from nil to non-nil after server creation (e.g.,
return an error) — update New, SetValidator, authInterceptor, and
streamAuthInterceptor accordingly to implement one consistent strategy.

---

Outside diff comments:
In `@internal/worker/handlers_auth.go`:
- Around line 313-325: Swagger annotations for handleCreateToken (and the other
handlers protected by requireSessionAdmin such as the list/revoke/stats
handlers) are out of sync: requireSessionAdmin now can return a 401 and returns
JSON for 403 rather than a plain string. Update the godoc comments for
handleCreateToken and the list/revoke/stats handlers to add a `@Failure` 401 entry
that documents a JSON error response (e.g. {object} map[string]interface{} or
your shared ErrorResponse type) and change the existing `@Failure` 403 to document
a JSON object (not string); ensure the documented response schema matches the
actual error payload your auth guard returns.

---

Duplicate comments:
In `@internal/auth/validator.go`:
- Around line 127-133: Кратко: текущая строка Client(candidates[i].Scope,
candidates[i].ID) напрямую превращает сырой текст из api_tokens.scope в роль и
позволяет эскалацию; нужно заменить это на безопасную валидацию/маппинг строки в
заранее определённый набор ролей. Исправьте код в месте сравнения токена (в
блоке, где используется candidates и вызывается Client): вместо прямого
приведения строки реализуйте/вызовите функцию вроде parseRole(scope string) или
mapScopeToRole(scope string) которая проверяет значение против белого списка
допустимых ролей (например "user","admin",...) и возвращает конкретный тип/enum
Role или ошибку/дефолтную роль; при неизвестном или недопустимом значении
отказывайте в аутентификации или назначайте минимальные права, не позволяя
произвольной строке пройти в Identity.Role.

In `@internal/handlers/serverevents/bridge.go`:
- Around line 108-113: The bridge currently reads EnvServerURLAlt into serverURL
first and only then falls back to EnvServerURL, which reverses the canonical
priority; change the logic in bridge.go so serverURL is populated from
config.EnvServerURL (ENGRAM_URL) first and only if empty use
config.EnvServerURLAlt (ENGRAM_SERVER_URL), and update the logger.Warn message
to reflect the correct deprecated/primary variable names; refer to the serverURL
variable and the EnvServerURL/EnvServerURLAlt symbols when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0d1663f5-b691-4604-bf3a-4c0fed7f6209

📥 Commits

Reviewing files that changed from the base of the PR and between 2feb662 and 93b45e5.

📒 Files selected for processing (7)
  • internal/auth/validator.go
  • internal/auth/validator_test.go
  • internal/grpcserver/server.go
  • internal/handlers/serverevents/bridge.go
  • internal/worker/handlers_auth.go
  • internal/worker/middleware.go
  • ui/package.json.tpl
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/auth/validator_test.go
  • internal/worker/middleware.go

Comment thread internal/auth/validator.go Outdated
Comment thread internal/grpcserver/server.go
…d test

Adds tests/critical/auth_two_tier_test.go (//go:build critical) — the first
@critical end-to-end test in this repo. Resolves the MISSING_SUITE verdict
from the critical-suite skill before v6.0.0 release.

The test exercises the v6 auth boundary across BOTH transports (gRPC + HTTP)
using the SAME validator chain — which is precisely the surface PR #203
silently broke. With this test in place, a future regression where gRPC and
HTTP diverge on validator wiring fails CI before the release tag is cut.

Sub-tests:
- gRPC bufconn server accepts operator key (FR-1 tier 1)
- gRPC bufconn server accepts dashboard-issued keycard (FR-2 symmetry — the
  assertion PR #203 would have flipped to FAIL)
- gRPC rejects garbage / missing bearer with Unauthenticated
- HTTP httptest server bearer arm: master + keycard accepted, garbage rejected
- Anti-stub mutation guard: replacing the keycard store with always-fail
  flips the keycard-success assertion, proving the test exercises the store
  path and is not a tautology

Run via: go test -tags=critical ./tests/critical/...
Build tag gates the suite away from default `go test ./...`.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/critical/auth_two_tier_test.go (2)

76-80: Уберите магические индексы 7:15 при расчёте префикса токена.

Сейчас префикс зависит от захардкоженных срезов; при изменении формата токена тест сломается неочевидно. Лучше вычислять через константы auth.TokenRawPrefix и auth.TokenPrefixLen.

Предлагаемое исправление
 	const (
 		operatorKey = "operator-secret-do-not-reuse"
 		clientRaw   = "engram_critic1c000000000000ffeed"
 	)
+	prefixStart := len(auth.TokenRawPrefix)
+	prefixEnd := prefixStart + auth.TokenPrefixLen
+	clientPrefix := clientRaw[prefixStart:prefixEnd]
@@
 	store := &critStore{
 		rows: map[string][]gormdb.APIToken{
-			clientRaw[7:15]: {{
+			clientPrefix: {{
 				ID:          "uuid-critical-1",
 				Name:        "critical-test-keycard",
 				TokenHash:   string(hash),
-				TokenPrefix: clientRaw[7:15],
+				TokenPrefix: clientPrefix,
 				Scope:       "read-write",
 			}},
 		},
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/critical/auth_two_tier_test.go` around lines 76 - 80, The test uses a
hardcoded slice clientRaw[7:15] to build TokenPrefix which is brittle; replace
the magic indices by computing the start as len(auth.TokenRawPrefix) and the end
as start + auth.TokenPrefixLen so TokenPrefix is derived as
clientRaw[start:end]; update the expectation where TokenPrefix is set (in the
table/struct literal using clientRaw[7:15]) to use that computed slice and
import/reference auth.TokenRawPrefix and auth.TokenPrefixLen accordingly.

136-151: Проверяйте gRPC-ошибки по коду статуса, а не по подстроке.

require.Contains(t, err.Error(), "Unauthenticated") хрупко к изменениям текста ошибки. Для стабильности и согласованности с остальной кодовой базой используйте status.Code(err) == codes.Unauthenticated. Потребуется добавить импорты:

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

Этот паттерн используется во всех тестах gRPC-обработчиков в проекте.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/critical/auth_two_tier_test.go` around lines 136 - 151, The tests in
auth_two_tier_test.go check gRPC errors by substring (require.Contains(t,
err.Error(), "Unauthenticated")) which is fragile; update both test cases (the
anonymous t.Run blocks that call client.Initialize) to assert the gRPC status
code using status.Code(err) == codes.Unauthenticated (or require.Equal(t,
codes.Unauthenticated, status.Code(err))) and add the imports
"google.golang.org/grpc/codes" and "google.golang.org/grpc/status" to the test
file so the status.Code and codes constants are available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/critical/auth_two_tier_test.go`:
- Line 7: В комментарии строки "//\tgo test -tags=critical ./tests/critical/..."
добавьте рядом стандартный прогон базового набора тестов — вставьте либо вторую
комментированную строку "//\tgo test ./..." либо объедините в одну строку, чтобы
оба запуска присутствовали; найдите эту строку в
tests/critical/auth_two_tier_test.go и обновите комментарий соответственно.
- Around line 245-246: The call to grpcserver.New is ignoring its error return
(srv, _ := grpcserver.New(stubMCPHandler{}, v)); change this to capture and
check the error from grpcserver.New (e.g., srv, err := grpcserver.New(...)) and
fail fast when err != nil by returning the error or calling t.Fatalf /
log.Fatalf with the error message so the test fails with a clear cause instead
of letting Serve() later fail mysteriously; update the code around
grpcserver.New and the goroutine that calls srv.Serve(lis) accordingly.

---

Nitpick comments:
In `@tests/critical/auth_two_tier_test.go`:
- Around line 76-80: The test uses a hardcoded slice clientRaw[7:15] to build
TokenPrefix which is brittle; replace the magic indices by computing the start
as len(auth.TokenRawPrefix) and the end as start + auth.TokenPrefixLen so
TokenPrefix is derived as clientRaw[start:end]; update the expectation where
TokenPrefix is set (in the table/struct literal using clientRaw[7:15]) to use
that computed slice and import/reference auth.TokenRawPrefix and
auth.TokenPrefixLen accordingly.
- Around line 136-151: The tests in auth_two_tier_test.go check gRPC errors by
substring (require.Contains(t, err.Error(), "Unauthenticated")) which is
fragile; update both test cases (the anonymous t.Run blocks that call
client.Initialize) to assert the gRPC status code using status.Code(err) ==
codes.Unauthenticated (or require.Equal(t, codes.Unauthenticated,
status.Code(err))) and add the imports "google.golang.org/grpc/codes" and
"google.golang.org/grpc/status" to the test file so the status.Code and codes
constants are available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b345f61d-f16e-4777-9b8b-df81d2c1fa97

📥 Commits

Reviewing files that changed from the base of the PR and between 93b45e5 and 6212d64.

📒 Files selected for processing (1)
  • tests/critical/auth_two_tier_test.go

Comment thread tests/critical/auth_two_tier_test.go
Comment thread tests/critical/auth_two_tier_test.go
First emulation playbook for engram, scoped to v6.0.0 auth-two-tier-tokens
release surfaces (server, plugin install, CLI proxy, dashboard tokens).

Resolves the MISSING verdict from the emulation-playbook skill before
v6.0.0 release. Future releases extend the scenario list per
/emulation-playbook --add.

Customer-mode walkthrough on this commit produced verdict
PASS_WITH_DEFERRED:
- Build, FR-4 fail-fast (server side), FR-4 fail-fast (client side) all PASS
- Two UX surprises noted (no --help handler, hardcoded keycard URL in FATAL
  message) — queued as low-effort follow-ups, not in scope of v6.0.0
- Three scenarios (live-stand happy path, dashboard /tokens, plugin install)
  deferred to user-driven walkthrough before tag — they do not exercise the
  v6 auth contract; they exercise unchanged infrastructure surfaces

Run report: .agent/reports/emulation-playbook-run-2026-04-26.md (gitignored,
working state).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/PRODUCTION-TESTING-PLAYBOOK.md (1)

124-124: Точка диагностики для /api/auth/tokens 500` выглядит смещенной

Для сигнала про /api/auth/tokens логичнее первым местом проверки указать HTTP-слой (internal/worker/handlers_auth.go и связанный middleware/validator), а не internal/grpcserver/server.go. Иначе расследование пойдет в нерелевантный путь.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/PRODUCTION-TESTING-PLAYBOOK.md` at line 124, Строка диагностики для
`/api/auth/tokens` 500 указывает неправильно приоритет проверки; обновите
таблицу так, чтобы первым местом проверки был HTTP-слой: укажите
`internal/worker/handlers_auth.go` и связанный middleware/validator (включая
функции/методы валидации, используемые в middleware) как первичную точку
диагностики, а уже затем `internal/grpcserver/server.go` / `SetValidator` как
вторичную проверку, чтобы расследование начиналось с релевантного кода.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/PRODUCTION-TESTING-PLAYBOOK.md`:
- Line 71: В failure signals текст упоминает deprecated-переменную как
ENGRAM_AUTH_TOKEN, но релиз/миграция использует ENGRAM_AUTH_ADMIN_TOKEN ↔
ENGRAM_TOKEN; замените или уточните упоминание на точное имя переменной,
используемое в кодовой базе/миграции (например, ENGRAM_AUTH_ADMIN_TOKEN если это
действительно deprecated имя, либо ENGRAM_TOKEN если это новое имя) и добавьте
краткую пометку о переименовании (old → new) чтобы исключить двусмысленность при
диагностике.
- Line 60: В шаге S2 замените неверное имя переменной окружения
ENGRAM_DATABASE_URL на правильное DATABASE_DSN (используется в блоке env
overrides в internal/config/config.go), чтобы сценарий использовал ту же
переменную, что и серверная конфигурация; обновите описание шага и любые
примеры/README в docs/PRODUCTION-TESTING-PLAYBOOK.md, чтобы ссылались на
DATABASE_DSN (или поясните, что можно оставить пустым для sqlite по умолчанию).

---

Nitpick comments:
In `@docs/PRODUCTION-TESTING-PLAYBOOK.md`:
- Line 124: Строка диагностики для `/api/auth/tokens` 500 указывает неправильно
приоритет проверки; обновите таблицу так, чтобы первым местом проверки был
HTTP-слой: укажите `internal/worker/handlers_auth.go` и связанный
middleware/validator (включая функции/методы валидации, используемые в
middleware) как первичную точку диагностики, а уже затем
`internal/grpcserver/server.go` / `SetValidator` как вторичную проверку, чтобы
расследование начиналось с релевантного кода.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4acb4cb6-72dd-4164-b9f9-dee4b547d2b6

📥 Commits

Reviewing files that changed from the base of the PR and between 6212d64 and ec74b2d.

📒 Files selected for processing (1)
  • docs/PRODUCTION-TESTING-PLAYBOOK.md

Comment thread docs/PRODUCTION-TESTING-PLAYBOOK.md Outdated
Comment thread docs/PRODUCTION-TESTING-PLAYBOOK.md Outdated
Addresses 3 of the 9 unresolved review comments on PR #208 — the highest-
impact correctness ones:

CRIT (CHANGELOG.md): the bullet referenced `.agent/specs/auth-two-tier-
tokens/spec.md`, a path under .agent/ which is gitignored project-wide.
Removed the dangling reference; the bullet still describes the BREAKING
change without pretending the spec is committed.

MAJOR (internal/grpcserver/server.go): SetValidator(v) could not enable
auth after a server was started with validator==nil — the unary/stream
interceptors were registered conditionally at New() time, so a later
non-nil swap left RPCs unprotected. Fix: always register the
interceptors; bypass them at runtime when currentValidator() returns
nil. Construction-time and runtime-promotion paths now share one
strategy.

MAJOR (internal/auth/validator.go): shape gate accepted any token of the
form "engram_" + ≥8 chars, leaving a cheap probe vector that ran a DB
lookup + bcrypt loop on syntactically impossible candidates. Fix:
require exact length (TokenTotalLen = 39) AND that the body is pure
hex. Adds TokenTotalLen + isHex helper. TokenMinLen retained as a
deprecated alias for compatibility with v6-prerelease imports.

Test fixture rewrite — every test fixture now matches the strict
shape:
- internal/auth/validator_test.go (8 raw tokens)
- internal/grpcserver/auth_interceptor_test.go (3 raw tokens)
- tests/critical/auth_two_tier_test.go (1 raw token; also imports
  auth.TokenRawPrefix / auth.TokenPrefixLen instead of magic indices,
  resolving the matching nitpick).

Verification:
- go test ./... — full suite green
- go test -tags=critical ./tests/critical/... — 6/6 sub-tests green
- go build ./... — clean
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
tests/critical/auth_two_tier_test.go (2)

254-255: ⚠️ Potential issue | 🟠 Major

Не игнорируйте ошибку grpcserver.New(...).

На Line 254 ошибка отбрасывается; при проблеме инициализации тест падает позже и менее прозрачно.

Предлагаемый фикс
 	lis := bufconn.Listen(1 << 20)
-	srv, _ := grpcserver.New(stubMCPHandler{}, v)
+	srv, err := grpcserver.New(stubMCPHandler{}, v)
+	require.NoError(t, err)
 	go func() { _ = srv.Serve(lis) }()
 
-	conn, err := grpc.NewClient(
+	conn, err := grpc.NewClient(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/critical/auth_two_tier_test.go` around lines 254 - 255, Ошибка в том,
что результат вызова grpcserver.New(stubMCPHandler{}, v) игнорируется; нужно
проверить и обработать возвращаемую ошибку, чтобы тест падал с понятным
сообщением при неудачной инициализации. Измените создание сервера
(grpcserver.New) так, чтобы вынимать оба значения (srv, err) и при err вызывать
t.Fatalf или аналогичную фэйловую проверку (или require.NoError(t, err)) с
информативным сообщением; оставьте существующий запуск goroutine со
srv.Serve(lis) только после успешной проверки err.

7-7: ⚠️ Potential issue | 🟡 Minor

Добавьте в блок запуска тестов стандартный прогон go test ./....

Сейчас в инструкции есть только critical-run; добавьте рядом базовый прогон полного набора.

As per coding guidelines, **/*_test.go: Run tests using go test ./... command.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/critical/auth_two_tier_test.go` at line 7, Update the test-run comment
at the top that currently only shows "go test -tags=critical
./tests/critical/..." to also include the standard full-suite command; edit the
commented line (the string "//\tgo test -tags=critical ./tests/critical/...") in
tests/critical/auth_two_tier_test.go so it lists both commands, e.g. add "go
test ./..." alongside the critical-run instruction to satisfy the guideline that
tests in *_test.go include the standard "go test ./..." invocation.
internal/auth/validator.go (1)

159-159: ⚠️ Potential issue | 🟠 Major

Не принимайте Scope из БД как доверенный ввод.

На Line 159 произвольный api_tokens.scope напрямую превращается в Identity.Role. Нужно принимать только разрешённые scope и отклонять остальные.

Предлагаемый фикс
 		if err == nil {
-			return Client(candidates[i].Scope, candidates[i].ID), nil
+			switch Role(candidates[i].Scope) {
+			case RoleReadWrite, RoleReadOnly:
+				return Client(candidates[i].Scope, candidates[i].ID), nil
+			default:
+				return Identity{}, ErrInvalidCredentials
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/validator.go` at line 159, Не доверяйте полю
candidates[i].Scope из БД: вместо прямого возврата Client(candidates[i].Scope,
candidates[i].ID) валидируйте и маппьте значение scope на разрешённые роли;
добавьте список допустимых scope или мапу (например allowedScopes / scopeToRole)
и в функции, где используется candidates (ссылаясь на Client и candidates),
проверяйте что candidates[i].Scope присутствует в этой структуре и преобразуйте
в внутреннее значение роли (или возвращайте ошибку/отклоняйте токен) прежде чем
вызывать Client(..., candidates[i].ID).
🧹 Nitpick comments (1)
internal/auth/validator_test.go (1)

40-47: Синхронизируйте fixture-валидацию с константами auth.Token*.

Сейчас helper привязан к >=15 и raw[7:15], хотя production shape уже строгий. Лучше использовать auth.TokenRawPrefix, auth.TokenPrefixLen, auth.TokenTotalLen.

Вариант правки
 	require.NoError(t, err)
-	require.True(t, strings.HasPrefix(raw, "engram_"), "test fixture: raw must start with engram_")
-	require.GreaterOrEqual(t, len(raw), 15, "test fixture: raw must be ≥ 15 chars")
+	require.True(t, strings.HasPrefix(raw, auth.TokenRawPrefix), "test fixture: invalid token prefix")
+	require.Equal(t, auth.TokenTotalLen, len(raw), "test fixture: invalid token length")
 	return gormdb.APIToken{
 		ID:          id,
 		Name:        "test-" + id,
 		TokenHash:   string(hash),
-		TokenPrefix: raw[7:15],
+		TokenPrefix: raw[len(auth.TokenRawPrefix) : len(auth.TokenRawPrefix)+auth.TokenPrefixLen],
 		Scope:       scope,
 		Revoked:     revoked,
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/validator_test.go` around lines 40 - 47, The test helper
currently hardcodes the token shape (checks len>=15 and uses raw[7:15]) — update
it to use the production constants auth.TokenRawPrefix, auth.TokenPrefixLen and
auth.TokenTotalLen: assert strings.HasPrefix(raw, auth.TokenRawPrefix) and
len(raw) == auth.TokenTotalLen (or >= if tests expect ≥), compute the
TokenPrefix as raw[len(auth.TokenRawPrefix) :
len(auth.TokenRawPrefix)+auth.TokenPrefixLen] and assign that to
gormdb.APIToken.TokenPrefix; import the auth package if necessary and update
assertion messages to reference the constants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/auth/validator.go`:
- Around line 145-146: The call to v.store.FindByPrefix can panic when v.store
is nil; add a fail-closed nil check before that lookup in the validator method
containing the lines with v.store and FindByPrefix (e.g., where candidates, err
:= v.store.FindByPrefix(ctx, prefix) is called). If v.store == nil, return the
appropriate authentication failure/error immediately (and optionally log an
explanatory message) instead of proceeding to call FindByPrefix, ensuring the
function fails closed rather than panicking.

---

Duplicate comments:
In `@internal/auth/validator.go`:
- Line 159: Не доверяйте полю candidates[i].Scope из БД: вместо прямого возврата
Client(candidates[i].Scope, candidates[i].ID) валидируйте и маппьте значение
scope на разрешённые роли; добавьте список допустимых scope или мапу (например
allowedScopes / scopeToRole) и в функции, где используется candidates (ссылаясь
на Client и candidates), проверяйте что candidates[i].Scope присутствует в этой
структуре и преобразуйте в внутреннее значение роли (или возвращайте
ошибку/отклоняйте токен) прежде чем вызывать Client(..., candidates[i].ID).

In `@tests/critical/auth_two_tier_test.go`:
- Around line 254-255: Ошибка в том, что результат вызова
grpcserver.New(stubMCPHandler{}, v) игнорируется; нужно проверить и обработать
возвращаемую ошибку, чтобы тест падал с понятным сообщением при неудачной
инициализации. Измените создание сервера (grpcserver.New) так, чтобы вынимать
оба значения (srv, err) и при err вызывать t.Fatalf или аналогичную фэйловую
проверку (или require.NoError(t, err)) с информативным сообщением; оставьте
существующий запуск goroutine со srv.Serve(lis) только после успешной проверки
err.
- Line 7: Update the test-run comment at the top that currently only shows "go
test -tags=critical ./tests/critical/..." to also include the standard
full-suite command; edit the commented line (the string "//\tgo test
-tags=critical ./tests/critical/...") in tests/critical/auth_two_tier_test.go so
it lists both commands, e.g. add "go test ./..." alongside the critical-run
instruction to satisfy the guideline that tests in *_test.go include the
standard "go test ./..." invocation.

---

Nitpick comments:
In `@internal/auth/validator_test.go`:
- Around line 40-47: The test helper currently hardcodes the token shape (checks
len>=15 and uses raw[7:15]) — update it to use the production constants
auth.TokenRawPrefix, auth.TokenPrefixLen and auth.TokenTotalLen: assert
strings.HasPrefix(raw, auth.TokenRawPrefix) and len(raw) == auth.TokenTotalLen
(or >= if tests expect ≥), compute the TokenPrefix as
raw[len(auth.TokenRawPrefix) : len(auth.TokenRawPrefix)+auth.TokenPrefixLen] and
assign that to gormdb.APIToken.TokenPrefix; import the auth package if necessary
and update assertion messages to reference the constants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ccfbf81-dcf6-4f49-8737-9c0623de69a3

📥 Commits

Reviewing files that changed from the base of the PR and between ec74b2d and 0ff9853.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • internal/auth/validator.go
  • internal/auth/validator_test.go
  • internal/grpcserver/auth_interceptor_test.go
  • internal/grpcserver/server.go
  • tests/critical/auth_two_tier_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/grpcserver/server.go

Comment thread internal/auth/validator.go
…iew)

Addresses 5 more PR #208 review threads.

MAJOR (internal/auth/validator.go): keycard validation copied
api_tokens.scope verbatim into Identity.Role. A row with scope="admin"
(data corruption, malicious INSERT, future schema drift) would have
promoted a worker keycard to admin. Fix: whitelist read-write/read-only,
return an explicit "unexpected scope" error otherwise.

MAJOR (internal/handlers/serverevents/bridge.go): the bridge read
ENGRAM_SERVER_URL first, falling back to ENGRAM_URL — opposite to
internal/config/envnames.go which declares ENGRAM_URL canonical. A
mid-migration deploy with both vars set to different values would have
served tools/call from one backend and the serverevents bridge from
another. Aligned the precedence.

MAJOR (internal/handlers/engramcore/grpcpool.go): connKey indexed only
on (addr, tlsMode, tokenHash). Switching ENGRAM_TLS_CA from one
non-empty path to another left tlsMode="custom-ca" and reused the
connection bound to the old trust store. Added tlsCAHash (sha256 short
hash, not the path itself — pool keys end up in heap dumps) so distinct
custom-CA paths get distinct pool entries.

MAJOR (internal/worker/middleware.go): bearer-arm reduced every
validator error to 401 — including wrapped store/bcrypt failures. HTTP
clients lost the auth-store-down signal that the gRPC interceptor
already surfaces as codes.Internal. Fixed: errors.Is on
ErrEmptyToken/ErrInvalidCredentials/ErrRevoked → 401; everything else
→ 500 with a distinct log message ("auth: store/bcrypt failure"). The
errors import was missing and is now added.

MAJOR (internal/worker/middleware.go, Authentik autoprovision): pushed
back. The reviewer suggested creating Authentik-autoprovisioned users
as "admin". The codebase intentionally distinguishes "admin" from
"operator" (auth_handlers.go:428 enforces this 2-value list). Auto-
elevating SSO callers to admin is a security downgrade — any user
landing through the SSO endpoint would receive keycard-issuance
privileges. Existing admins promote new users via PATCH
/api/users/{id}/role. Inline comment added explaining the design;
behavior unchanged. v5 issuance was master-token-bearer-only, so
"operator" session users could not issue keycards in v5 either — no
regression.

MINOR (tests/critical/auth_two_tier_test.go): grpcserver.New returns
(*grpc.Server, *grpcserver.Server) — NOT (*grpc.Server, error). The
underscore is the *Server pointer, not a swallowed error. Inline
comment added so the next reviewer does not have to re-discover the
signature.

Verification:
- go test ./internal/... — full internal suite green (28 packages)
- go test -tags=critical ./tests/critical/... — 6/6 sub-tests green
- go build ./... — clean
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Apr 26, 2026

🤖 PR Review MCP State (auto-managed, do not edit)
{
  "version": 2,
  "parentChildren": {},
  "resolvedNitpicks": {
    "coderabbit-nitpick-fc403c45-76": {
      "resolvedAt": "2026-04-26T18:10:19.982Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-45762e5a-136": {
      "resolvedAt": "2026-04-26T18:10:28.689Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-bffd1f64-40": {
      "resolvedAt": "2026-04-26T18:13:31.070Z",
      "resolvedBy": "agent"
    }
  },
  "updatedAt": "2026-04-26T18:13:31.677Z"
}

CRIT (internal/auth/validator.go): Validate could panic with nil
pointer dereference when v.store == nil and a non-master token was
presented. Although production New() always passes a non-nil store,
the test surface and the bootstrap window can produce a nil-store
validator. Added explicit fail-closed branch returning
ErrInvalidCredentials before the FindByPrefix call.

MAJOR (docs/PRODUCTION-TESTING-PLAYBOOK.md): S2 step 2 referenced
`ENGRAM_DATABASE_URL` — not the canonical name. internal/config
reads `DATABASE_DSN`. Operators following the playbook would have
hit a non-reproducible path. Fixed.

MINOR (docs/PRODUCTION-TESTING-PLAYBOOK.md): the failure-signals
mention of a deprecated env var was ambiguous. Clarified that the
v6 server-host token is `ENGRAM_AUTH_ADMIN_TOKEN` (kept) and the
workstation rename was `ENGRAM_AUTH_ADMIN_TOKEN` → `ENGRAM_TOKEN`.

MINOR (internal/auth/validator_test.go makeKeycard helper): fixture
validation hardcoded `len >= 15` and `raw[7:15]` despite the
production shape now being strict (= TokenTotalLen + hex). Wired
the helper to auth.TokenRawPrefix / TokenPrefixLen / TokenTotalLen
so a future shape change cannot leave the test fixtures silently
green.

Verification:
- go test ./internal/auth/... — green
- go test -tags=critical ./tests/critical/... — 6/6 sub-tests green
@thebtf thebtf merged commit 21b4907 into main Apr 26, 2026
8 checks passed
@thebtf thebtf deleted the worktree-pr-auth-v6 branch April 26, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant