Compare commits
11 Commits
85a25fd995
...
v0.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e22c0fdeb | |||
| 1f4d54e474 | |||
| b7a39b45d7 | |||
| 1bfdc91f90 | |||
| 58a90ac9d7 | |||
| 684dbdc6a4 | |||
| e92de12cf9 | |||
| 1f784a6a04 | |||
| ab37c2e69f | |||
| c8f3e0db44 | |||
| 02772a3910 |
14
.env.example
14
.env.example
@@ -18,3 +18,17 @@ BETTER_AUTH_URL=http://localhost:4000
|
||||
|
||||
# Gateway
|
||||
GATEWAY_PORT=4000
|
||||
|
||||
# Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable)
|
||||
# DISCORD_BOT_TOKEN=
|
||||
# DISCORD_GUILD_ID=
|
||||
# DISCORD_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
# Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable)
|
||||
# TELEGRAM_BOT_TOKEN=
|
||||
# TELEGRAM_GATEWAY_URL=http://localhost:4000
|
||||
|
||||
# Authentik SSO (optional — set AUTHENTIK_CLIENT_ID to enable)
|
||||
# AUTHENTIK_ISSUER=https://auth.example.com
|
||||
# AUTHENTIK_CLIENT_ID=
|
||||
# AUTHENTIK_CLIENT_SECRET=
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
variables:
|
||||
- &node_image 'node:22-alpine'
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
- &enable_pnpm 'corepack enable'
|
||||
|
||||
when:
|
||||
- event: [push, pull_request, manual]
|
||||
|
||||
# Steps run sequentially to avoid OOM on the CI runner.
|
||||
# node_modules is installed once by the install step and shared across
|
||||
# all subsequent steps via Woodpecker's shared workspace volume.
|
||||
|
||||
steps:
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- corepack enable
|
||||
- pnpm install --frozen-lockfile
|
||||
|
||||
typecheck:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- *enable_pnpm
|
||||
- pnpm typecheck
|
||||
depends_on:
|
||||
- install
|
||||
@@ -24,34 +27,31 @@ steps:
|
||||
lint:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- *enable_pnpm
|
||||
- pnpm lint
|
||||
depends_on:
|
||||
- install
|
||||
- typecheck
|
||||
|
||||
format:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- *enable_pnpm
|
||||
- pnpm format:check
|
||||
depends_on:
|
||||
- install
|
||||
- lint
|
||||
|
||||
test:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- *enable_pnpm
|
||||
- pnpm test
|
||||
depends_on:
|
||||
- install
|
||||
- format
|
||||
|
||||
build:
|
||||
image: *node_image
|
||||
commands:
|
||||
- *install_deps
|
||||
- *enable_pnpm
|
||||
- pnpm build
|
||||
depends_on:
|
||||
- typecheck
|
||||
- lint
|
||||
- format
|
||||
- test
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@mosaic/coord": "workspace:^",
|
||||
"@mosaic/db": "workspace:^",
|
||||
"@mosaic/discord-plugin": "workspace:^",
|
||||
"@mosaic/telegram-plugin": "workspace:^",
|
||||
"@mosaic/log": "workspace:^",
|
||||
"@mosaic/memory": "workspace:^",
|
||||
"@mosaic/types": "workspace:^",
|
||||
|
||||
@@ -86,4 +86,52 @@ describe('Resource ownership checks', () => {
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
|
||||
it('forbids creating a task with an unowned project', async () => {
|
||||
const brain = createBrain();
|
||||
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||
const controller = new TasksController(brain as never);
|
||||
|
||||
await expect(
|
||||
controller.create(
|
||||
{
|
||||
title: 'Task',
|
||||
projectId: 'project-1',
|
||||
},
|
||||
{ id: 'user-1' },
|
||||
),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('forbids listing tasks for an unowned project', async () => {
|
||||
const brain = createBrain();
|
||||
brain.projects.findById.mockResolvedValue({ id: 'project-1', ownerId: 'user-2' });
|
||||
const controller = new TasksController(brain as never);
|
||||
|
||||
await expect(
|
||||
controller.list({ id: 'user-1' }, 'project-1', undefined, undefined),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('lists only tasks for the current user owned projects when no filter is provided', async () => {
|
||||
const brain = createBrain();
|
||||
brain.projects.findAll.mockResolvedValue([
|
||||
{ id: 'project-1', ownerId: 'user-1' },
|
||||
{ id: 'project-2', ownerId: 'user-2' },
|
||||
]);
|
||||
brain.missions.findAll.mockResolvedValue([{ id: 'mission-1', projectId: 'project-1' }]);
|
||||
brain.tasks.findAll.mockResolvedValue([
|
||||
{ id: 'task-1', projectId: 'project-1' },
|
||||
{ id: 'task-2', missionId: 'mission-1' },
|
||||
{ id: 'task-3', projectId: 'project-2' },
|
||||
]);
|
||||
const controller = new TasksController(brain as never);
|
||||
|
||||
await expect(
|
||||
controller.list({ id: 'user-1' }, undefined, undefined, undefined),
|
||||
).resolves.toEqual([
|
||||
{ id: 'task-1', projectId: 'project-1' },
|
||||
{ id: 'task-2', missionId: 'mission-1' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,15 @@ async function bootstrap(): Promise<void> {
|
||||
throw new Error('BETTER_AUTH_SECRET is required');
|
||||
}
|
||||
|
||||
if (
|
||||
process.env['AUTHENTIK_CLIENT_ID'] &&
|
||||
(!process.env['AUTHENTIK_CLIENT_SECRET'] || !process.env['AUTHENTIK_ISSUER'])
|
||||
) {
|
||||
console.warn(
|
||||
'[warn] AUTHENTIK_CLIENT_ID is set but AUTHENTIK_CLIENT_SECRET or AUTHENTIK_ISSUER is missing — Authentik SSO will not work',
|
||||
);
|
||||
}
|
||||
|
||||
const logger = new Logger('Bootstrap');
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
|
||||
@@ -36,7 +36,10 @@ export class MissionsController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() dto: CreateMissionDto) {
|
||||
async create(@Body() dto: CreateMissionDto, @CurrentUser() user: { id: string }) {
|
||||
if (dto.projectId) {
|
||||
await this.getOwnedProject(dto.projectId, user.id, 'Mission');
|
||||
}
|
||||
return this.brain.missions.create({
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { DiscordPlugin } from '@mosaic/discord-plugin';
|
||||
import { TelegramPlugin } from '@mosaic/telegram-plugin';
|
||||
import { PluginService } from './plugin.service.js';
|
||||
import type { IChannelPlugin } from './plugin.interface.js';
|
||||
|
||||
@@ -26,9 +27,23 @@ class DiscordChannelPluginAdapter implements IChannelPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
class TelegramChannelPluginAdapter implements IChannelPlugin {
|
||||
readonly name = 'telegram';
|
||||
|
||||
constructor(private readonly plugin: TelegramPlugin) {}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.plugin.start();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.plugin.stop();
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_GATEWAY_URL = 'http://localhost:4000';
|
||||
|
||||
function createPluginRegistry(logger: Logger): IChannelPlugin[] {
|
||||
function createPluginRegistry(): IChannelPlugin[] {
|
||||
const plugins: IChannelPlugin[] = [];
|
||||
const discordToken = process.env['DISCORD_BOT_TOKEN'];
|
||||
const discordGuildId = process.env['DISCORD_GUILD_ID'];
|
||||
@@ -50,8 +65,13 @@ function createPluginRegistry(logger: Logger): IChannelPlugin[] {
|
||||
const telegramGatewayUrl = process.env['TELEGRAM_GATEWAY_URL'] ?? DEFAULT_GATEWAY_URL;
|
||||
|
||||
if (telegramToken) {
|
||||
logger.warn(
|
||||
`Telegram plugin requested for ${telegramGatewayUrl}, but @mosaic/telegram-plugin is not implemented yet.`,
|
||||
plugins.push(
|
||||
new TelegramChannelPluginAdapter(
|
||||
new TelegramPlugin({
|
||||
token: telegramToken,
|
||||
gatewayUrl: telegramGatewayUrl,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +83,7 @@ function createPluginRegistry(logger: Logger): IChannelPlugin[] {
|
||||
providers: [
|
||||
{
|
||||
provide: PLUGIN_REGISTRY,
|
||||
useFactory: (): IChannelPlugin[] => createPluginRegistry(new Logger('PluginModule')),
|
||||
useFactory: (): IChannelPlugin[] => createPluginRegistry(),
|
||||
},
|
||||
PluginService,
|
||||
],
|
||||
|
||||
@@ -28,17 +28,48 @@ export class TasksController {
|
||||
|
||||
@Get()
|
||||
async list(
|
||||
@CurrentUser() user: { id: string },
|
||||
@Query('projectId') projectId?: string,
|
||||
@Query('missionId') missionId?: string,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
if (projectId) return this.brain.tasks.findByProject(projectId);
|
||||
if (missionId) return this.brain.tasks.findByMission(missionId);
|
||||
if (status)
|
||||
return this.brain.tasks.findByStatus(
|
||||
status as Parameters<typeof this.brain.tasks.findByStatus>[0],
|
||||
);
|
||||
return this.brain.tasks.findAll();
|
||||
if (projectId) {
|
||||
await this.getOwnedProject(projectId, user.id, 'Task');
|
||||
return this.brain.tasks.findByProject(projectId);
|
||||
}
|
||||
if (missionId) {
|
||||
await this.getOwnedMission(missionId, user.id, 'Task');
|
||||
return this.brain.tasks.findByMission(missionId);
|
||||
}
|
||||
|
||||
const [projects, missions, tasks] = await Promise.all([
|
||||
this.brain.projects.findAll(),
|
||||
this.brain.missions.findAll(),
|
||||
status
|
||||
? this.brain.tasks.findByStatus(
|
||||
status as Parameters<typeof this.brain.tasks.findByStatus>[0],
|
||||
)
|
||||
: this.brain.tasks.findAll(),
|
||||
]);
|
||||
|
||||
const ownedProjectIds = new Set(
|
||||
projects.filter((project) => project.ownerId === user.id).map((project) => project.id),
|
||||
);
|
||||
const ownedMissionIds = new Set(
|
||||
missions
|
||||
.filter(
|
||||
(ownedMission) =>
|
||||
typeof ownedMission.projectId === 'string' &&
|
||||
ownedProjectIds.has(ownedMission.projectId),
|
||||
)
|
||||
.map((ownedMission) => ownedMission.id),
|
||||
);
|
||||
|
||||
return tasks.filter(
|
||||
(task) =>
|
||||
(task.projectId ? ownedProjectIds.has(task.projectId) : false) ||
|
||||
(task.missionId ? ownedMissionIds.has(task.missionId) : false),
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -47,7 +78,13 @@ export class TasksController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() dto: CreateTaskDto) {
|
||||
async create(@Body() dto: CreateTaskDto, @CurrentUser() user: { id: string }) {
|
||||
if (dto.projectId) {
|
||||
await this.getOwnedProject(dto.projectId, user.id, 'Task');
|
||||
}
|
||||
if (dto.missionId) {
|
||||
await this.getOwnedMission(dto.missionId, user.id, 'Task');
|
||||
}
|
||||
return this.brain.tasks.create({
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
**ID:** mvp-20260312
|
||||
**Statement:** Build Mosaic Stack v0.1.0 — a self-hosted, multi-user AI agent platform with web dashboard, TUI, remote control, shared memory, mission orchestration, and extensible skill/plugin architecture. All TypeScript. Pi as agent harness. Brain as knowledge layer. Queue as coordination backbone.
|
||||
**Phase:** Execution
|
||||
**Current Milestone:** Phase 5: Remote Control (v0.0.6)
|
||||
**Progress:** 5 / 8 milestones
|
||||
**Current Milestone:** Phase 6: CLI & Tools (v0.0.7)
|
||||
**Progress:** 6 / 8 milestones
|
||||
**Status:** active
|
||||
**Last Updated:** 2026-03-13 UTC
|
||||
**Last Updated:** 2026-03-14 UTC
|
||||
|
||||
## Success Criteria
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
| 2 | ms-159 | Phase 2: Agent Layer (v0.0.3) | done | — | — | 2026-03-13 | 2026-03-12 |
|
||||
| 3 | ms-160 | Phase 3: Web Dashboard (v0.0.4) | done | — | — | 2026-03-12 | 2026-03-13 |
|
||||
| 4 | ms-161 | Phase 4: Memory & Intelligence (v0.0.5) | done | — | — | 2026-03-13 | 2026-03-13 |
|
||||
| 5 | ms-162 | Phase 5: Remote Control (v0.0.6) | not-started | — | — | — | — |
|
||||
| 5 | ms-162 | Phase 5: Remote Control (v0.0.6) | done | — | #99 | 2026-03-14 | 2026-03-14 |
|
||||
| 6 | ms-163 | Phase 6: CLI & Tools (v0.0.7) | not-started | — | — | — | — |
|
||||
| 7 | ms-164 | Phase 7: Polish & Beta (v0.1.0) | not-started | — | — | — | — |
|
||||
|
||||
@@ -68,7 +68,8 @@
|
||||
| 7 | claude-opus-4-6 | 2026-03-12 | — | context limit | P2-007 |
|
||||
| 8 | claude-opus-4-6 | 2026-03-12 | — | context limit | Phase 2 complete |
|
||||
| 9 | claude-opus-4-6 | 2026-03-12 | — | context limit | P3-007 |
|
||||
| 10 | claude-opus-4-6 | 2026-03-13 | — | active | P3-008 |
|
||||
| 10 | claude-opus-4-6 | 2026-03-13 | — | context limit | P3-008 |
|
||||
| 11 | claude-opus-4-6 | 2026-03-14 | — | active | P5-005 |
|
||||
|
||||
## Scratchpad
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
| P4-005 | done | Phase 4 | Memory integration — inject into agent sessions | — | #38 |
|
||||
| P4-006 | done | Phase 4 | Skill management — catalog, install, config | — | #39 |
|
||||
| P4-007 | done | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 |
|
||||
| P5-001 | not-started | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 |
|
||||
| P5-001 | done | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 |
|
||||
| P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |
|
||||
| P5-003 | not-started | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 |
|
||||
| P5-004 | not-started | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 |
|
||||
| P5-005 | not-started | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | — | #45 |
|
||||
| P5-003 | done | Phase 5 | @mosaic/telegram-plugin — Telegraf bot + channel plugin | — | #43 |
|
||||
| P5-004 | done | Phase 5 | SSO — Authentik OIDC adapter end-to-end | — | #44 |
|
||||
| P5-005 | done | Phase 5 | Verify Phase 5 — Discord + Telegram + SSO working | #99 | #45 |
|
||||
| P6-001 | not-started | Phase 6 | @mosaic/cli — unified CLI binary + subcommands | — | #46 |
|
||||
| P6-002 | not-started | Phase 6 | @mosaic/prdy — migrate PRD wizard from v0 | — | #47 |
|
||||
| P6-003 | not-started | Phase 6 | @mosaic/quality-rails — migrate scaffolder from v0 | — | #48 |
|
||||
|
||||
40
docs/plans/authentik-sso-setup.md
Normal file
40
docs/plans/authentik-sso-setup.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Authentik SSO Setup
|
||||
|
||||
## Create the Authentik application
|
||||
|
||||
1. In Authentik, create an OAuth2/OpenID Provider.
|
||||
2. Create an Application and link it to that provider.
|
||||
3. Copy the generated client ID and client secret.
|
||||
|
||||
## Required environment variables
|
||||
|
||||
Set these values for the gateway/auth runtime:
|
||||
|
||||
```bash
|
||||
AUTHENTIK_CLIENT_ID=your-client-id
|
||||
AUTHENTIK_CLIENT_SECRET=your-client-secret
|
||||
AUTHENTIK_ISSUER=https://authentik.example.com
|
||||
```
|
||||
|
||||
`AUTHENTIK_ISSUER` should be the Authentik base URL, for example `https://authentik.example.com`.
|
||||
|
||||
## Redirect URI
|
||||
|
||||
Configure this redirect URI in the Authentik provider/application:
|
||||
|
||||
```text
|
||||
{BETTER_AUTH_URL}/api/auth/callback/authentik
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
https://mosaic.example.com/api/auth/callback/authentik
|
||||
```
|
||||
|
||||
## Test the flow
|
||||
|
||||
1. Start the gateway with `BETTER_AUTH_URL` and the Authentik environment variables set.
|
||||
2. Open the Mosaic login flow and choose the Authentik provider.
|
||||
3. Complete the Authentik login.
|
||||
4. Confirm the browser returns to Mosaic and a session is created successfully.
|
||||
@@ -73,6 +73,19 @@ User confirmed: start the planning gate.
|
||||
| ------- | ---------- | --------- | ---------- | ----------------------------------------------------------------------------------- |
|
||||
| 7-8 | 2026-03-12 | Phase 2 | P2-007 | 19 unit tests (routing + coord). PR #79 merged, issue #25 closed. Phase 2 complete. |
|
||||
|
||||
### Session 11 — Phase 5 completion
|
||||
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
| ------- | ---------- | --------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 11 | 2026-03-14 | Phase 5 | P5-005 | Wired Telegram plugin into gateway (was stubbed). Updated .env.example with all P5 env vars. PR #99 merged, issue #45 closed. Phase 5 complete. |
|
||||
|
||||
**Findings during verification:**
|
||||
|
||||
- Telegram plugin was built but not wired into gateway (stub warning in plugin.module.ts)
|
||||
- Discord plugin was fully wired
|
||||
- SSO/Authentik OIDC adapter was fully wired
|
||||
- All three quality gates passing
|
||||
|
||||
## Open Questions
|
||||
|
||||
(none at this time)
|
||||
|
||||
44
docs/scratchpads/p5-004-authentik-sso.md
Normal file
44
docs/scratchpads/p5-004-authentik-sso.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# P5-004 Scratchpad
|
||||
|
||||
- Objective: Add optional Authentik OIDC SSO adapter via Better Auth genericOAuth.
|
||||
- Task ref: P5-004
|
||||
- Issue ref: #96
|
||||
- Plan:
|
||||
1. Inspect auth/gateway surfaces and Better Auth plugin shape.
|
||||
2. Add failing coverage for auth config/startup validation where feasible.
|
||||
3. Implement adapter, docs, and warnings.
|
||||
4. Run targeted typechecks, lint, and review.
|
||||
|
||||
- TDD note: no low-friction auth plugin or bootstrap-env test seam exists for `packages/auth/src/auth.ts` or `apps/gateway/src/main.ts`. This change is configuration-oriented and does not alter an existing behavioral contract with a current test harness. I skipped new tests for this pass and relied on exact typecheck/lint/test commands plus manual review.
|
||||
|
||||
- Changes:
|
||||
1. Added conditional Better Auth `genericOAuth` plugin registration for the `authentik` provider in `packages/auth/src/auth.ts`.
|
||||
2. Added a soft startup warning in `apps/gateway/src/main.ts` for incomplete Authentik env configuration.
|
||||
3. Added `docs/plans/authentik-sso-setup.md` with env, redirect URI, and test-flow guidance.
|
||||
4. Confirmed `packages/auth/src/index.ts` already exports `AuthConfig`; no change required there.
|
||||
|
||||
- Verification:
|
||||
1. `pnpm --filter @mosaic/db build`
|
||||
2. `pnpm --filter @mosaic/auth typecheck`
|
||||
3. `pnpm --filter @mosaic/gateway typecheck`
|
||||
4. `pnpm lint`
|
||||
5. `pnpm format:check`
|
||||
6. `pnpm --filter @mosaic/auth test`
|
||||
7. `pnpm --filter @mosaic/gateway test`
|
||||
|
||||
- Results:
|
||||
1. `@mosaic/auth` typecheck passed after replacing the non-existent `enabled` field with conditional plugin registration.
|
||||
2. `@mosaic/gateway` typecheck passed.
|
||||
3. Repo lint passed.
|
||||
4. Prettier check passed after formatting `apps/gateway/src/main.ts`.
|
||||
5. `@mosaic/auth` tests reported `No test files found, exiting with code 0`.
|
||||
6. `@mosaic/gateway` tests passed: `3` files, `20` tests.
|
||||
|
||||
- Review:
|
||||
1. Manual review of the diff found no blocker issues.
|
||||
2. External `codex-code-review.sh --uncommitted` was attempted but did not return a usable verdict in-session; no automated review findings were available from that run.
|
||||
|
||||
- Situational evidence:
|
||||
1. Provider activation is env-gated by `AUTHENTIK_CLIENT_ID`.
|
||||
2. Misconfigured optional SSO surfaces a warning instead of crashing gateway startup.
|
||||
3. Setup doc records the expected redirect path: `{BETTER_AUTH_URL}/api/auth/callback/authentik`.
|
||||
58
docs/scratchpads/task-mission-ownership-20260313.md
Normal file
58
docs/scratchpads/task-mission-ownership-20260313.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Task Ownership Gap Fix Scratchpad
|
||||
|
||||
## Metadata
|
||||
|
||||
- Date: 2026-03-13
|
||||
- Worktree: `/home/jwoltje/src/mosaic-mono-v1-worktrees/fix-task-ownership`
|
||||
- Branch: `fix/task-mission-ownership`
|
||||
- Scope: Fix ownership checks in TasksController/MissionsController and extend gateway ownership tests
|
||||
- Related tracker: worker task only; `docs/TASKS.md` is orchestrator-owned and left unchanged
|
||||
- Budget assumption: no explicit token cap; keep scope limited to requested gateway permission fixes
|
||||
|
||||
## Objective
|
||||
|
||||
Close ownership gaps so task listing/creation and mission creation enforce project/mission ownership and reject cross-user access.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. TasksController `list()` enforces ownership for `projectId` and `missionId`, and does not return cross-user data when neither filter is provided.
|
||||
2. TasksController `create()` rejects unowned `projectId` and `missionId` references.
|
||||
3. MissionsController `create()` rejects unowned `projectId` references.
|
||||
4. Gateway ownership tests cover forbidden task creation and forbidden task listing by unowned project.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Inspect current controller and ownership test patterns.
|
||||
2. Add failing permission tests first.
|
||||
3. Patch controller methods with existing ownership helpers.
|
||||
4. Run targeted gateway tests, then gateway typecheck/lint/full test.
|
||||
5. Perform independent review, record evidence, then complete the requested git/PR workflow.
|
||||
|
||||
## TDD Notes
|
||||
|
||||
- Required: yes. This is auth/permission logic and a bugfix.
|
||||
- Strategy: add failing tests in `resource-ownership.test.ts`, verify red, then implement minimal controller changes.
|
||||
|
||||
## Verification Log
|
||||
|
||||
- `pnpm --filter @mosaic/gateway test -- src/__tests__/resource-ownership.test.ts`
|
||||
- Red: failed with 2 expected permission-path failures before controller changes.
|
||||
- Green: passed after wiring ownership checks and adding owned-task filtering coverage.
|
||||
- `pnpm --filter @mosaic/gateway typecheck`
|
||||
- Pass on 2026-03-13 after fixing parameter ordering and mission project nullability.
|
||||
- `pnpm --filter @mosaic/gateway lint`
|
||||
- Pass on 2026-03-13.
|
||||
- `pnpm --filter @mosaic/gateway test`
|
||||
- Pass on 2026-03-13 with 3 test files and 23 tests passing.
|
||||
- `pnpm format:check`
|
||||
- Pass on 2026-03-13.
|
||||
|
||||
## Review Log
|
||||
|
||||
- Manual review: checked for auth regressions, cross-user list leakage, and dashboard behavior impact; kept unfiltered task list functional by filtering to owned projects/missions instead of returning an empty list.
|
||||
- Automated review: `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted` running/re-run for independent review evidence.
|
||||
|
||||
## Risks / Blockers
|
||||
|
||||
- Repository-wide Mosaic instructions require merge/issue closure, but the user explicitly instructed PR-only and no merge; follow the user instruction.
|
||||
- `docs/TASKS.md` is orchestrator-owned and will not be edited from this worker task.
|
||||
@@ -1,5 +1,6 @@
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||
import { genericOAuth } from 'better-auth/plugins';
|
||||
import type { Db } from '@mosaic/db';
|
||||
|
||||
export interface AuthConfig {
|
||||
@@ -10,6 +11,33 @@ export interface AuthConfig {
|
||||
|
||||
export function createAuth(config: AuthConfig) {
|
||||
const { db, baseURL, secret } = config;
|
||||
const authentikIssuer = process.env['AUTHENTIK_ISSUER'];
|
||||
const authentikClientId = process.env['AUTHENTIK_CLIENT_ID'];
|
||||
const authentikClientSecret = process.env['AUTHENTIK_CLIENT_SECRET'];
|
||||
const plugins = authentikClientId
|
||||
? [
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: 'authentik',
|
||||
clientId: authentikClientId,
|
||||
clientSecret: authentikClientSecret ?? '',
|
||||
discoveryUrl: authentikIssuer
|
||||
? `${authentikIssuer}/.well-known/openid-configuration`
|
||||
: undefined,
|
||||
authorizationUrl: authentikIssuer
|
||||
? `${authentikIssuer}/application/o/authorize/`
|
||||
: undefined,
|
||||
tokenUrl: authentikIssuer ? `${authentikIssuer}/application/o/token/` : undefined,
|
||||
userInfoUrl: authentikIssuer
|
||||
? `${authentikIssuer}/application/o/userinfo/`
|
||||
: undefined,
|
||||
scopes: ['openid', 'email', 'profile'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
: undefined;
|
||||
|
||||
return betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
@@ -36,6 +64,7 @@ export function createAuth(config: AuthConfig) {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
updateAge: 60 * 60 * 24, // refresh daily
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -71,6 +71,9 @@ importers:
|
||||
'@mosaic/memory':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/memory
|
||||
'@mosaic/telegram-plugin':
|
||||
specifier: workspace:^
|
||||
version: link:../../plugins/telegram
|
||||
'@mosaic/types':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/types
|
||||
|
||||
Reference in New Issue
Block a user