security(authz): grant-ceiling for role create/assign + bearer aud/scope (PACA-3/4, C1)#292
security(authz): grant-ceiling for role create/assign + bearer aud/scope (PACA-3/4, C1)#292phancao wants to merge 41 commits into
Conversation
…ias paca-gateway, pinned 0.9.7 images, HDD backups Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
Adds an optional OIDC authorization-code login (PKCE S256 +
client_secret_post) against the Vortex issuer, off unless OIDC_ISSUER
is set:
- migration 000022: nullable users.email / users.oidc_sub with partial
unique indexes (WHERE ... IS NOT NULL)
- internal/platform/oidc: lazy cached discovery, kid-keyed JWKS cache
with rate-limited refresh on unknown kid, RS256 verification
(iss/aud/exp), HMAC-signed state+PKCE cookie codec
- GET /api/v1/auth/oidc/login + /callback: state cookie, code exchange,
id_token verification, then the exact same session-cookie issuance as
password login (authsvc.IssueSession, shared with Login)
- user resolution: find by oidc_sub -> link by email -> JIT-create
(OIDC_AUTO_CREATE_USERS, default role via OIDC_DEFAULT_ROLE resolved
against global_roles; unusable random password)
- GET /api/v1/auth/config (public) advertises {oidc_enabled,
oidc_button_label} to the SPA
- apps/web: primary 'Sign in with Vortex' button above the local login
form when enabled; i18n keys added to all 9 locales (incl. vi)
Env: OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URL
(defaults from PUBLIC_URL), OIDC_SCOPES, OIDC_AUTO_CREATE_USERS,
OIDC_DEFAULT_ROLE, OIDC_BUTTON_LABEL.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…-switch (ADR-038 P1)
Identity from signed tokens, never headers:
- GALAXY_TRUSTED_ISSUER: the API additionally accepts RS256 bearer
tokens verified against that issuer's JWKS (iss/exp/alg). Effective
principal = act_as claim (object {sub} or plain string, parsed
defensively) falling back to token sub; mapped to users.oidc_sub with
NO auto-create — unknown principals get 401. act_as_agent is recorded
and logged for attribution only and never feeds the authz agent-role
path, so an acting agent can never exceed the principal user's
permissions. HS256 bearers (own sessions) are untouched.
RequireJWTAuth routes (API-key mgmt, password change) also reject
platform bearers: acting agents must not mint long-lived credentials.
- AGENT_HEADER_IMPERSONATION kill-switch (default DISABLED): pairing
the shared AGENT_API_KEY with an X-Agent-ID header now returns 401
with an error naming ADR-038 unless the env is explicitly 'enabled'.
Authenticators without an impersonation policy are treated as
disabled (fail closed). The shared key alone (no header) still
authenticates the ai-agent service as the bot user.
- apps/mcp fail-closed tool filter: fetchAgentPermissions now THROWS on
any fetch failure (network or non-OK) instead of returning empty maps
('all tools shown' per old README); createServer keeps starting but
ListTools retries once and then surfaces an error, exposing ZERO
tools until permissions are known. Filter extracted to
filterToolsByPermissions + unit tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
… socket-proxy (ADR-038 P1) - services/ai-agent fails fast at startup (clear FATAL log + exit 1) when INTERNAL_API_KEY is empty or whitespace; pydantic validation errors are printed as field+message only (never values). The Go API has no INTERNAL_API_KEY counterpart: empty AGENT_API_KEY already disables the agent-key path entirely (verified, nothing to change). - LLM_BASE_URL_OVERRIDE / LLM_API_KEY_OVERRIDE take precedence over the per-agent llm_base_url / API key stored in the DB, logging a notice naming the agent — prod can force all LLM traffic through the platform proxy. - DOCKER_HOST support: docker client construction goes through docker_daemon_url() — DOCKER_HOST (tcp://...) wins over the legacy DOCKER_SOCKET bare path (scheme-carrying values pass through too). - deploy/galaxy overlay: new socket-proxy service (tecnativa/docker-socket-proxy, CONTAINERS/IMAGES/NETWORKS/EXEC/ VOLUMES/POST=1, ro sock mount, stack-private network, 128m) mediating ai-agent's Docker access; ai-agent gets DOCKER_HOST=tcp://socket-proxy:2375, volumes '!override []' (no raw sock bind), and the two LLM override envs. API service gets the ADR-038 identity envs (OIDC_*, GALAXY_TRUSTED_ISSUER, AGENT_HEADER_IMPERSONATION=disabled). .env.galaxy.example documents all of it; ai-agent stays scaled to 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…ation kill, ai-agent hardening Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
- GET /api/v1/auth/config now advertises dock_enabled + dock_src (env GALAXY_DOCK_SRC, empty = disabled) alongside the OIDC fields. - SPA mounts <galaxy-chat-dock app-id="paca"> from the authenticated layout only, lazily loading the bundle from dock_src after login — same element/attributes as the Galaxy AI Wiki integration. - Ports the wiki dock-bootstrap SSO relay: one bounce per browser session through the portal /dock-sso page, token returned in a URL fragment, consumed+scrubbed at app boot before render. - Caddy gateway bridges /dock.js + /api/agentops* + /api/identity* same-origin to the platform gateway over galaxy_network (GALAXY_GATEWAY_URL, default http://vortex-gateway) so the dock's relative API calls work without CORS and SSE stays unbuffered — mirrors Galaxy-AI-Wiki galaxyDockProxy. - Env plumbing in the galaxy overlay compose + .env.galaxy.example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
… (ADR-038 P3.1)
Self-contained Python 3.12 sidecar (deploy/galaxy/notify-bridge/) that
tails Paca's own Valkey streams with a separate consumer group
(galaxy-notify-bridge, created at $ — no history replay):
- paca.task_assignments (task.assigned — the same stream Paca's
NotificationConsumer turns into 'assigned' notification rows)
- paca.plugin_events (task.comment.added — mentions are derived
synchronously in AddComment, so comments are the only durable
mention source; paca.task_activities carries neither signal)
Mirrors Paca's notifications semantics exactly: agent assignees and
self-assignments skipped, structured BlockNote teamMention ids with
legacy @username fallback, membership + dedup + self-mention rules.
Recipients map to Vortex subs via READ-ONLY Postgres lookups on
users.oidc_sub (session forced default_transaction_read_only=on);
unlinked users are skipped with a debug log.
Publishes the house fan-out schema (pm_service notify_emit.py /
notification-service): {user_ids:[sub], type, title, body, deeplink,
payload, source_app:paca} on notify.fan-out via the platform Redis
(GALAXY_NOTIFY_REDIS_URL, default redis://agentops-redis:6379/0).
Deeplinks use the SPA's real route:
https://tasks.skyplatform.net/projects/<pid>/tasks/<tid>.
Compose: joins default + galaxy_network, mem 256m, restart
unless-stopped, idles unless BRIDGE_ENABLED=true (or scale to 0).
Tests: 29 pytest cases over the pure mapping/filter functions —
uv run --with pytest,redis,"psycopg[binary]" pytest deploy/galaxy/notify-bridge/tests -q
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
deploy/galaxy/tenant-template/:
- provision-tenant-paca.sh <tenant_code> <domain> renders
~/Nexus/paca-tenants/<code>/.env.tenant (mode 600) from
env.tenant.template: __TENANT__/__DOMAIN__ replaced, a distinct
openssl rand -hex 32 for every secret, COMPOSE_PROJECT_NAME=
paca-<code>, GATEWAY_NETWORK_ALIAS=paca-<code>-gateway,
OIDC_CLIENT_ID=paca-<code>, BACKUP_DIR=/backup/paca-<code>-postgres.
Refuses to overwrite live tenant env; prints the compose invocation
(-p paca-<code>, SAME repo compose files) + manual checklist
(Vortex OIDC client, Cloudflare ingress to http://paca-<code>-gateway:80,
DNS CNAME, first-backup verify).
- README.md documents the ADR-038 T7 isolation model.
Isolation verified against the real compose merge (-p paca-vietjet):
all 8 named volumes have no explicit name/external and no service sets
container_name, so they materialize as paca-<code>_*; default network
becomes paca-<code>_default; gateway keeps zero host ports.
The gateway's galaxy_network alias was hardcoded in the overlay --
now ${GATEWAY_NETWORK_ALIAS:-paca-gateway}, defaulting to the shared
instance's existing alias (no behavior change there).
Syntax-checked with bash noexec mode; shellcheck (stable, via docker) clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…late Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…-038 T6)
v1 thin frontend-only Paca plugin: module-federation remote whose surfaces
iframe the existing SDD sensor UI (nexus.8verse.games/sdd-server, which
enforces its own Vortex OIDC auth). No secrets, no real backend.
- plugin.json: view + project.page/navItems (routed page sdd-fleet, sidebar
nav) + sidebar.project.section card; remoteEntryUrl per the /plugins/<id>/
assets/remoteEntry.js store convention. "backend": {} kept non-null on
purpose (Runtime.EmitEvent nil-derefs Manifest.Backend).
- frontend: vite 8 + @module-federation/vite 1.16.8 remote sharing react/
react-dom ^19 (host share scope singletons); classic JSX runtime + class
component so the bundle stays correct even on shared-fallback; iframe with
sandbox allow-same-origin+scripts, onload-timer fallback overlay telling the
operator to unset X-Frame-Options / allow frame-ancestors
https://tasks.skyplatform.net on the sensor; permanent open-in-new-tab
escape hatch. smoke.mjs replays the host loader init/get contract against
the built dist (passes; both components render).
- backend/: inert dependency-free wasip1 c-shared stub — Runtime.Load
unconditionally requires backend.wasm for enabled plugins; stub (and the
8-byte no-Go fallback) verified to load under wazero 1.11.0.
- build.sh validates the manifest (mirrors entity.go + plugin-api.ts — no
formal schema exists), builds + smokes both halves; install-prod.sh docker-
cps the stores into the galaxy-paca named volumes then registers via
POST/PATCH /api/v1/admin/plugins; dev keeps working with the stock
scripts/install-local-plugin.sh unchanged.
- deploy/galaxy/README.md: plugin section + CSP finding (gateway sets no CSP
today → nothing to allowlist; blank frame = fix the sensor side).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…e-name alias 'gateway' collided with vortex-gateway on galaxy_network (portal outage 17/07) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…8 P3.4)
Plugin deploy/galaxy/plugins/com.galaxy.analytics — native Module
Federation analytics views (no iframe, no chart libs):
- Four panels computed client-side from same-origin /api/v1 REST with the
caller's own session (credentials: include; tasks cursor-paginated at
page_size=100; sprints + task-statuses; shared fetch module with 60s
in-memory cache and in-flight dedup):
* Sprint Progress — honest burndown v1: hero %, meter, ideal line
start->end + today's actual remaining only (no history), task-count
fallback when a sprint has no points
* Velocity — points on done-category tasks per completed sprint, avg line
* Status Distribution — CFD v1: stacked counts by status category for
backlog + live sprints, legend with visible counts
* Sprint Report — per-sprint table (done/attached, open, points); "Open"
dashed for completed sprints (Paca moves unfinished tasks out)
All no-history approximations documented in the UI footnote.
- Dependency-free inline SVG charts (dataviz method: validated palette both
themes, 2px surface gaps/rings, selective labels, tooltips + table so no
value is hover-gated); theme rides host tokens + light/dark class.
- Architecture cloned from com.galaxy.sdd: non-null "backend": {} manifest
+ inert Go wasip1 stub backend.wasm, same build.sh validation, same
install-prod.sh volume flow, classic JSX runtime, bare react imports,
class components, smoke.mjs loader-contract replay (bare render + fixture
render asserting every panel, forbidding iframes).
- Extension points: view + project.page + navItems (slug analytics,
label "Analytics"); remoteEntry.js 14.0 KB (4.9 KB gzip).
Skills deploy/galaxy/skills — PM-service AI prompts (pm_service
routers/ai_service.py) adapted to Paca MCP tools, attach inline per agent:
- galaxy-sprint-health: verdict 🟢/🟡/🔴 + 3 câu (tiến độ/rủi ro/khuyến
nghị), tiếng Việt mặc định, pace = points vs timebox
- galaxy-triage: gợi ý epic (parentTaskId), importance theo bucket số
(0/10/35/75/150), tags từ vocabulary sẵn có; confirm trước khi ghi
- galaxy-estimate: Fibonacci 1..21 + confidence + lý do 1 câu, hiệu chuẩn
theo task done của dự án; không lặng lẽ ghi đè estimate cũ
- skills/README.md: runbook gắn skill qua UI/API (paste body dưới
frontmatter), lưu ý LLM_BASE_URL_OVERRIDE ép traffic qua platform proxy
(ADR-038 T3) — không cấp key LLM riêng cho agent
deploy/galaxy/README.md: ghi hai mục mới (plugin + skills).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…etwork; login auto-redirects to Vortex SSO (?local=1 break-glass) id_token iss = http://nexus-identity:8086 (identity's internal OIDC_ISSUER, same contract RAGFlow uses); public-issuer config was rejected with 'invalid issuer'. Also per platform design: no app-local login screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…ve portal path - logout → /?logged_out=1: login screen stays put (no auto-SSO bounce), offers re-login + ADR-027 platform-wide sign-out link - com.galaxy.sdd: nexus.8verse.games is dead externally (not in tunnel ingress) → https://ai.skyplatform.net/sdd-server (same-site cookies flow, no X-Frame-Options on that path — verified) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…were edge-stale for 15m Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…adel rejects unregistered URIs; identity default dest (portal login) is registered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
… TTL to 4h, query bump busts stale edge copies Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…atform AI proxy with per-user attribution (ADR-038 T3) When GALAXY_AI_ROLE is set (e.g. paca-ai), build_llm sends model=openai/<role> to identity /ai/v1 — the proxy resolves the role via ai_role_assignments, so the platform admin rebinds the model in /nexus/admin → AI Models with no Paca redeploy. Auth is a per-conversation RS256 act_as token minted from /internal/mint-service-token (X-Service-Secret), naming the TRIGGERING user (email over UUID) with act_as_agent=<role> — mirrors the PM attribution contract (pm_service/utils/attribution.py). Fail-closed: no fleet JWT_SECRET fallback, startup refuses role-without-secret, mint failure fails the conversation with the real cause. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…networks, kept out of the primary slot The OpenHands SDK serializes the agent spec (LLM config included) into the agent-server, so in galaxy mode the SANDBOX makes the LLM calls and must reach nexus-identity over galaxy_network. Extra networks are connected right after create (before the readiness wait) and EXCLUDED from primary-network selection: ai-agent itself now sits on two networks and Docker lists them alphabetically — without the exclusion the sandbox's primary network (which must resolve api/gateway) would depend on how the names sort. A typo'd network fails the start loudly; the existing cleanup path handles it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…network as paca-ai-agent; restore stack-side gateway alias - ai-agent: galaxy_network with explicit alias paca-ai-agent (implicit ai-agent alias collision-scanned 17/07 — clear; nearest is galaxy-ai-agent-service alias ai_service, distinct) + GALAXY_* env plumbing + SANDBOX_EXTRA_NETWORKS. - .env.galaxy.example: GALAXY_AI_ROLE=paca-ai defaults; the platform INTERNAL_SERVICE_SECRET is referenced host-side at deploy time, never committed; raw LLM_*_OVERRIDE demoted to commented-out legacy. - paca-edge: alias gateway on the STACK network only — the base compose points ai-agent (GATEWAY_BASE_URL) and sandbox MCP bundle URLs at http://gateway, which stopped resolving when the gateway service was profile-disabled in the vortex-gateway collision fix. - README: ops runbook for the role, attribution, the 900s token ceiling, and the sandbox network design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…e (ADR-038 T3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
Replace the per-request X-API-Key/X-Agent-ID identity headers with a single Authorization: Bearer platform token behind PACA_AUTH_MODE (default apikey = upstream behavior, thin diff). In bearer mode the principal comes from the token's signed act_as claim and agent attribution from act_as_agent — no identity headers are ever sent (the Galaxy API rejects header impersonation by design, AGENT_HEADER_IMPERSONATION=disabled). - src/auth.ts: buildAuthHeaders/registerAuthResponse + resolveAuthEnv. Fail-fast at startup when bearer mode lacks PACA_MCP_TOKEN. - 401 latch: the first bearer 401 throws a clear do-not-retry error and every subsequent request short-circuits before the network — a static token cannot become valid again, so retrying only burns agent iterations (the MaxIterations loop bug). - index.ts drops PACA_AGENT_ID in bearer mode so no code path can send it; permissions fetch + plugin loader use the same auth builder (permission filtering stays fail-closed on fetch failure). - Tests: both header modes, invalid/missing env, expired-token latch behavior (629 passing), bun run build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…ss on JWKS-verified bearers Identity's /internal/mint-service-token stamps service/act_as tokens with iss="galaxy-nexus" (hardcoded; callers cannot override it), while its OIDC discovery/JWKS live at GALAXY_TRUSTED_ISSUER (http://nexus-identity:8086). The strict WithIssuer check therefore rejected every legitimately minted platform token: 'token has invalid issuer' (verified live against prod). Fix: NewProviderWithIssuerClaims — an explicit allow-list of ADDITIONAL iss claim values accepted on tokens whose signature verifies against the trusted issuer's JWKS. The trust anchor is unchanged (signature still checked exclusively against GALAXY_TRUSTED_ISSUER's keys); only the string equality of the iss claim is relaxed to the configured list. Default empty = strict as before; the SSO id_token path is untouched (plain NewProvider). Deploy: set GALAXY_TRUSTED_ISSUER_CLAIMS=galaxy-nexus in .env.galaxy. Tests: extra claim accepted, unlisted iss rejected, forged token with an allow-listed iss but a foreign key rejected, strict mode unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…H_MODE=bearer) Alongside the LLM act_as token, mint a SECOND RS256 platform token for the built-in Paca MCP server and deliver it into the sandbox via the serialized MCP env (the same channel PACA_API_KEY used) as PACA_MCP_TOKEN with PACA_AUTH_MODE=bearer — no PACA_API_KEY, no PACA_AGENT_ID (the API rejects header impersonation by design). Contract differences from the LLM token (verified against prod): - act_as = the triggering user's Vortex OIDC SUB, not email — Paca's galaxy_bearer resolves principals via users.oidc_sub (cao.phan's sub is a UUID; an email would never match). No email fallback: non-SSO actors get a token with no act_as → the API fails closed → the MCP server surfaces a clear zero-tools error (never impersonation). - aud = paca-api (the Go verifier checks iss/exp/signature, not aud). - TTL = conversation_timeout_seconds + 10 min buffer, explicitly capped at identity's hard 900s service-token clamp (C2 bound in identity_service/api/internal.py) — asking for more is silently shortened, so we cap rather than pretend. Also: conversation_timeout_seconds setting (default 3600) now drives the executor's polling timeout and the MCP TTL computation. Tests: oidc_sub-only resolution, TTL cap, aud/act_as contract, fail-closed non-SSO path, bearer env shape incl. no legacy credentials (190 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…book (ADR-038) - deploy/galaxy/agent-server/Dockerfile: multi-stage build of the fork's apps/mcp bundle (bun frozen-lockfile deps, node tsc build, production-only node_modules) overlaid onto ghcr.io/paca-ai/paca-agent-server:0.9.7 at /usr/local/lib/node_modules/@paca-ai/paca-mcp (verified layout: global install + /usr/local/bin/paca symlink; npx resolves it offline). Build on the prod host with context apps/mcp; pin via AGENT_SERVER_IMAGE with a -galaxy.N suffix bumped per rebuild. - README: principal policy — fail-closed, no bot user. Tool write-backs run with exactly the triggering user's permissions (act_as = users.oidc_sub); agents must be triggered by SSO users; non-SSO actors → 401 → zero tools with a clear non-retrying error. Header impersonation stays disabled. Documents GALAXY_TRUSTED_ISSUER_CLAIMS and the 900s identity TTL clamp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…ns as openhands) The upstream image's default user cannot modify /usr/local/lib/node_modules; switch to root for the overlay layers, chmod the tree world-readable, and switch back to openhands so runtime behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…ds npm/node absent from the bun image Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…i service env Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…ks as the triggering user (ADR-038) E2E-verified on prod 17/07: task GXP-9 assigned by cao.phan (RS256 bearer, act_as=oidc_sub) → Paca PO sandbox (galaxy-local/paca-agent-server:0.9.7-galaxy.1) → MCP bearer mode exposed the USER's tools → real Sprint-1-summary comment written via POST /activities/comments logged as agent=paca-ai user=cao.phan; ai_usage_logs attributed on_behalf_of=cao.phan@galaxytechnology.vn; legacy X-API-Key+X-Agent-ID still 401 by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…ck surface) - dock-trigger: notify-bridge sibling (own consumer group galaxy-dock-agent) — task assigned to / @mention of DOCK_AGENT_TRIGGER_USERNAMES (galaxy-tasks-agent) runs the AgentOps paca assistant AS the assigner/mentioner (RS256 mint, sub=users.oidc_sub) and the agent comments the result back via paca_* tools. At-most-once (ack-before-dispatch); loop guards for agent-actored events; unit tests. - ai-agent + socket-proxy: profiles [retired-use-chatdock] (gateway pattern) — default up never starts them; source untouched (thin-fork). README: agent surface = ChatDock, OpenHands path retired-but-dormant; deploy command drops --scale ai-agent=0. - .env.galaxy.example: DOCK_TRIGGER_* keys + missing GALAXY_TRUSTED_ISSUER_CLAIMS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…rectory sync
Admin user APIs (POST /admin/users, PATCH /admin/users/{id}) accept optional
email + oidc_sub so the platform directory-sync reconciler can pre-provision
every Vortex user PRE-LINKED to the identity provider (unique partial indexes
from migration 000022; conflicts surface as 409 USER_IDENTITY_TAKEN). Creation
writes both columns in the same INSERT (atomic); PATCH goes through a
dedicated SetOIDCIdentity that can set/correct but never clear a link, and a
row bound to a different subject 409s instead of being reassigned. An account
created WITH oidc_sub is SSO-first: must_change_password=false (the flag rides
in JWT claims and would lock an SSO login out of every route).
JIT login (galaxyauth) untouched — find-by-sub reuses the synced row, no dup
(regression test added).
users.is_service (new migration 000023): marks non-human service/bridge
accounts; exposed on user + project-member payloads; Team page and member
picker render a neutral "Service" badge (i18n en+vi, defaultValue fallback).
Badge only — service members are never hidden.
go build/vet green; go test ./... green; web tsc + vitest green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…Is (ADR-038 T2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
…lugin (ADR-038) Add the sdd-proxy sidecar + gateway route so the native com.galaxy.sdd plugin can read SDD fleet telemetry same-origin, with no iframe and no secret in the browser: - deploy/galaxy/sdd-proxy/: zero-dependency Node sidecar. Per request it (1) gates on the caller's Paca session (GET api:8080/api/v1/users/me; else 401), (2) injects a short-lived RS256 service token minted at nexus-identity /internal/mint-service-token (iss=galaxy-nexus, TTL<=900s, mirrors dock-trigger; optional HS256 fallback via SDD_SHARED_JWT_SECRET), and (3) reverse-proxies GET reads to sdd-server:4830/api/*. GET/HEAD only — team-wide READ; writes 405. Secrets never logged. - deploy/caddy/Caddyfile: handle_path /sdd-api/* -> sdd-proxy:8791 (distinct prefix, no collision with /api/*). - deploy/galaxy/docker-compose.galaxy.yml: sdd-proxy service on default + galaxy_network (alias paca-sdd-proxy); paca-edge depends_on it. SDD central/auth.js requireRead already accepts the minted RS256 token via JWKS — no SDD-side change needed. Verified locally: node --check, path-mapping + HS256 signer unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
Rewrite com.galaxy.sdd from an iframe wrapper into a native Module Federation plugin (clones com.galaxy.analytics's structure): one "SDD Fleet" project page with a left sub-rail of eight team-wide views + client-side sub-routing, each fetching same-origin /sdd-api/* (shared 60s cache) and rendering dependency-free inline-SVG/CSS cards, tables, bars, spark and timelines — theme-aware, i18n vi/en/zh. No iframe, no chart libraries, no secrets. Views ported from the standalone client/src/pages: Overview (TeamDashboard), Task board (TeamKanban, read-only), Sessions, Activity, Analytics (TeamAnalytics), Coordination (TeamCoordination), SDD phases (Sdd — 8-phase board + L1–L4 governance mix + spec versions + flags), Fleet (TeamFleet). - frontend/src: config, types, sdd-api (the only fetch module), i18n, icons (inline SVG, no lucide), theme (gxsd-, light+dark), base (DataView class + primitives), views (8 views + registry), SddFleetView (shell). - Removed the iframe sources (SddFrame.tsx, SddSidebarCard.tsx); vite exposes only SddFleetView; dropped the sidebar.project.section extension point. - plugin.json → v0.2.0, remoteEntryUrl ?v=3 (CF cache-bust); build.sh lint now allows the ?v=N query. - smoke.mjs asserts all 8 view keys render + zero <iframe + seeded overview. - Class components + classic JSX (no hooks) for federation React-copy safety. Build verified locally: tsc + vite federation build + smoke pass; grep confirms zero "iframe" in the built dist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
… + decommission) New docs/runbooks/galaxy-paca-runbook.md: stack invocation, the native com.galaxy.sdd plugin + /sdd-api proxy data path, build/deploy (local build → ship dist → docker cp + DB upsert, no api restart), CF ?v cache-bust, verify + rollback, and the SDD standalone decommission steps (SDD_SERVE_SPA=off, launcher status=inactive). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhWa7S4kPwmiuSW3cmMF6a
Vertical-authorization (RBAC) remediation — see AUTHZ-AUDIT-2026-07-18. PACA-3 (CRITICAL) global-role self-escalation: globalrole service Create/Update/ReplaceUserRoles now reject any role whose permission set exceeds the caller's own effective global permissions (ErrPermissionCeilingExceeded -> 403). Caller permissions are threaded from the handler, which resolves them via the authorizer; "*"/SUPER_ADMIN bypasses. PACA-4 (CRITICAL) project-role self-escalation via shared PROJECT_OWNER template: project member service AddMember/UpdateMemberRoleByMemberID now reject assigning a role broader than the caller's effective project permissions. The handler resolves the caller's project-scoped set (agent- and user-aware). PACA-C1 (HIGH) audience/scope confused-deputy on trusted-issuer bearer path: bearer VerifyToken is now called with Paca's configured resource audience (GALAXY_BEARER_AUDIENCE), and the token scope claim is enforced against the operation — foreign-resource tokens are rejected and read-only tokens are denied write methods (GALAXY_RESOURCE_SCOPE_PREFIX, default "mcp:paca:"). Method is threaded from the GalaxyBearer middleware. Shared ceiling logic lives in platform/authz (PermissionSet, EffectivePermissions, PermissionsFromMap); HasPermissions refactored onto the same core. Adds fail-before/pass-after unit tests for PACA-3, PACA-4 and PACA-C1. go build/test ./... green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UgVbGm9LF8V2NygJ9Aqudv
There was a problem hiding this comment.
Important
Two enforcement gaps should be closed before merge: UpdateMemberRole has no PACA-4 grant-ceiling check, and an empty GALAXY_RESOURCE_SCOPE_PREFIX silently disables PACA-C1 scope enforcement.
Core PACA-3/4 and C1 work is sound — grant ceilings are correctly shared via authz.Ceil/IsCoveredBy, audience enforcement is opt-in, X-Agent-ID impersonation is default-disabled, and go build ./... / go test ./... pass for services/api. Please see the inline comments for the required changes.
Other (non-blocking) observations:
oidc.Provider.fetchJWKSLockedsilently skips malformed JWKS entries and caches an empty keyset; consider logging warnings or failing on an empty JWKS.- Plugin proxy authentication is intentionally handled inside
ProxyRequest, so it correctly bypasses the new Galaxy bearer group middleware.
Note: 1 inline comment(s) dropped because they did not anchor to lines inside the PR diff:
services/api/internal/service/project/project_member_service.go:84(RIGHT) — line 84 (RIGHT) is not inside a diff hunk
Kimi K2 (free via Pullfrog for OSS) | 𝕏
| // foreign-resource token) and, for write methods, at least one non-read-only | ||
| // Paca scope. | ||
| func (a *BearerAuthenticator) enforceScope(claims jwt.MapClaims, method string) error { | ||
| if a.resourceScopePrefix == "" { |
There was a problem hiding this comment.
Empty prefix should not be treated as "scope check disabled" when audience enforcement is also optional. A misconfigured deployment can accidentally disable both PACA-C1 audience and scope validation, creating a fail-open path. Please reject an empty GALAXY_RESOURCE_SCOPE_PREFIX at startup when bearer auth is enabled, or return an error here so the scope check fails closed.
|

Summary
Vertical-authorization (RBAC) remediation for Galaxy-Paca. Full report:
Galaxy-Nexus/docs/AUTHZ-AUDIT-2026-07-18.md. Conceptual golden pattern: a grant ceiling — a caller must not create or assign a role whose permission set is broader than the caller's own effective permissions.PACA-3 (CRITICAL) — Global-role self-escalation
globalroleserviceCreate/Update/ReplaceUserRolesnever compared the created/assigned role's permissions to the caller's own. Now:Create/Updatereject any permission the caller does not hold.ReplaceUserRolesrejects unless every target role's permission set is a subset of the caller's own effective permissions.*/SUPER_ADMIN bypasses. Rejections return403 FORBIDDEN(ErrPermissionCeilingExceeded).GlobalRoleHandler(now wired with the authorizer).PACA-4 (CRITICAL) — Project-role self-escalation via shared
PROJECT_OWNERtemplateprojectmember serviceAddMember/UpdateMemberRoleByMemberIDverified only that the target role belonged to the project/was a template. Now they also reject a role whose permissions exceed the caller's project-scoped effective permissions (agent- and user-aware, threaded from the handler).PACA-C1 (HIGH) — Audience/scope confused-deputy on the trusted-issuer bearer path
bearer.gonow verifies the token with Paca's configured resource audience (GALAXY_BEARER_AUDIENCE) instead of an emptyexpectedAudience.mcp:paca:read) token is denied write methods. Prefix configurable viaGALAXY_RESOURCE_SCOPE_PREFIX(defaultmcp:paca:); the HTTP method is threaded from theGalaxyBearermiddleware. Tokens without a scope claim (identity's service/act_as tokens) are unaffected.Design
Shared ceiling logic added in
platform/authz/ceiling.go(PermissionSet,EffectivePermissions,EffectivePermissionsForAgent,PermissionsFromMap);HasPermissionsrefactored onto the same core so route-guard and ceiling can never diverge. Changes are minimal and fail-closed.Tests
Fail-before/pass-after unit tests added for PACA-3, PACA-4 (service layer) and PACA-C1 (bearer). Verified the reject-tests fail when the ceiling/scope checks are neutered. One pre-existing integration test (
TestIntegrationProjectRolesAndMembers_Flow) encoded the vulnerable behavior (caller assignedtasks.readit didn't hold) and was updated to a legitimately-authorized caller.go build ./...andgo test ./...are green (34 packages).🤖 Generated with Claude Code