feat: mattermost push notification integration and roadmap update

This commit is contained in:
toki 2026-05-29 21:24:59 +09:00
parent bbb5b98d60
commit e5f64f29b1
12 changed files with 274 additions and 14 deletions

View file

@ -30,9 +30,9 @@ NomadCode는 Flutter 기반 앱, core 서비스, 공유 계약, agent-operation
- [진행중] Workflow Core
- 경로: `agent-roadmap/phase/workflow-core/PHASE.md`
- 요약: Roadmap Driven Agent-Ops Automation을 NomadCode Core와 MCP-first 제어 표면 방향으로 정리한 뒤 실제 e2e 흐름을 기준으로 task lifecycle, retry, timeout, notification event를 안정화한다.
- [계획] External Integration
- [진행중] External Integration
- 경로: `agent-roadmap/phase/external-integration/PHASE.md`
- 요약: Plane 확장, Mattermost, Agent Integrator, IOP OpenAI API Responses-compatible 호출을 실제 통합 흐름으로 확장한다.
- 요약: Workflow Core 재개 전에 Mattermost/Nexo messaging 정합성을 먼저 닫고, 이후 Plane 확장, Mattermost, Agent Integrator, IOP OpenAI API Responses-compatible 호출을 실제 통합 흐름으로 확장한다.
- [계획] Project Workspace Management UX
- 경로: `agent-roadmap/phase/project-workspace-management-ux/PHASE.md`
- 요약: client integration 표준화는 완료했고, 실제 프로젝트 단위 앱 UX 구현은 core workflow와 외부 통합 기준 이후로 미룬다.

View file

@ -4,11 +4,14 @@
- [진행중] Workflow Core
- 경로: `agent-roadmap/phase/workflow-core/PHASE.md`
- [계획] External Integration
- [진행중] External Integration
- 경로: `agent-roadmap/phase/external-integration/PHASE.md`
## 활성 Milestone
- [진행중] Mattermost Nexo Messaging Alignment
- Phase: `agent-roadmap/phase/external-integration/PHASE.md`
- 경로: `agent-roadmap/phase/external-integration/milestones/mattermost-nexo-messaging-alignment.md`
- [계획] Roadmap Driven Agent-Ops Automation
- Phase: `agent-roadmap/phase/workflow-core/PHASE.md`
- 경로: `agent-roadmap/phase/workflow-core/milestones/roadmap-driven-agent-ops-automation.md`

View file

@ -2,7 +2,7 @@
## 상태
[계획]
[진행중]
## 목표
@ -17,6 +17,10 @@ Work Item Provider Pipeline Design과 Workflow Core 이후 Plane/Jira 확장, Ma
- 경로: `agent-roadmap/archive/phase/external-integration/milestones/mattermost-messaging-android-smoke.md`
- 요약: `../nexo/packages/messaging_flutter` 전환 이후 원격 Android SDK build, emulator runtime, Mattermost FCM 알림 도착, 웹 채널 메시지 도착을 확인했다.
- [진행중] Mattermost Nexo Messaging Alignment
- 경로: `agent-roadmap/phase/external-integration/milestones/mattermost-nexo-messaging-alignment.md`
- 요약: Workflow Core 재개 전에 Mattermost 메시지/알림 경계를 `../nexo/packages/messaging_flutter` 구조와 맞추고 server-generated signed push smoke 기준을 선행 검증한다.
- [계획] External Integration
- 경로: `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- 요약: Plane/Jira/Mattermost/Agent Integrator/IOP 호출 경계를 실제 통합 adapter 흐름으로 확장한다.
@ -24,6 +28,7 @@ Work Item Provider Pipeline Design과 Workflow Core 이후 Plane/Jira 확장, Ma
## Phase 경계
- 외부 provider 세부 구현은 adapter 경계 밖으로 새지 않게 한다.
- Mattermost Nexo Messaging Alignment는 현재 workflow/core 작업을 재개하기 전에 닫아야 하는 선행 정합성 작업이다.
- Mattermost messaging Android smoke는 사용자 테스트를 제외하고 원격 Android SDK build와 알림 도착 확인까지만 완료 범위로 둔다.
- IOP 내부 모델 라우팅, 모델 프로파일, RAG, MCP, output validation, fallback 정책은 NomadCode 범위에서 제외한다.
- A2A 도입 시점과 Agent Integrator 유지 여부는 사용자 결정이 필요하다.

View file

@ -39,7 +39,7 @@ Work Item Provider Pipeline Design과 workflow core 이후 남은 Plane/Jira 확
- [ ] [plane-adapter-expand] Plane issue 생성, comment, status update adapter 확장. 검증: core가 Plane에 issue, comment, status update를 요청할 수 있다.
- [ ] [jira-adapter] Jira issue 조회, comment, status transition adapter 구현. 검증: core가 Jira에 issue 조회, comment, status transition을 요청할 수 있다.
- [ ] [mattermost-adapter] Mattermost 메시지 발송 adapter 구현. 검증: core가 Mattermost에 메시지를 발송할 수 있다.
- [ ] [mattermost-adapter] Mattermost 메시지 발송 adapter 구현`../nexo/packages/messaging_flutter` host notification boundary 정합성 유지. 검증: core가 Mattermost에 메시지를 발송하고, server-generated signed push smoke가 `docs/test-environments.md` 기준으로 FCM/ACK/opened/reply/dismiss evidence를 남긴다.
- [ ] [agent-integrator] Agent Integrator 호출 경계 정의. 검증: Agent Integrator 또는 그 대체 연결 지점이 명확히 정의되어 있다.
- [x] [iop-responses] IOP OpenAI API Responses-compatible 경로를 NomadCode의 기본 실행 호출 경로로 정리. 검증: IOP OpenAI API Responses-compatible 호출 경로가 core workflow와 연결된다.
- [x] [model-reclass] direct model endpoint / Ollama fallback 표현과 설정을 IOP 경유 호출 기준으로 재분류. 검증: NomadCode의 기본 실행 경로가 직접 모델 호출이 아니라 IOP 경유 호출임이 로드맵과 운영 문서에서 일관되게 읽힌다.
@ -69,8 +69,13 @@ Work Item Provider Pipeline Design과 workflow core 이후 남은 Plane/Jira 확
- 이전 출처: `services/core/README.md``## 단계별 다음 작업`
- 주요 작업 영역: `services/core/internal/adapters/`, `services/core/internal/scheduler/`, `services/core/internal/workflow/`
- 선행 작업: Work Item Provider Pipeline Design, Workflow Core
- 선행 작업: Work Item Provider Pipeline Design, Workflow Core, Mattermost Nexo Messaging Alignment
- 후속 작업: Project Workspace Management UX
- Mattermost signed push smoke 재현 가이드: `docs/test-environments.md#mattermost-server-generated-push-smoke`
- private 환경값 router: `agent-ops/rules/private/rules.md` (ignored local file)
- Mattermost 책임 경계: core는 Mattermost REST 메시지 발송과 task notification 발행을 담당하고, `../nexo/packages/messaging_flutter`는 client-side FCM 수신, signature 검증, ACK, notification display, opened-routing, inline reply, dismiss를 담당한다.
- Nexo host 정합성: NomadCode host는 Firebase 설정, Mattermost credential handoff, signing key, optional server identifier, navigation callback만 `MattermostPushClient` 경계로 전달한다. native push 처리 로직은 `apps/client/android`에 복제하지 않는다.
- 선행 순서: Mattermost 메시지/알림 경계 정합성은 `agent-roadmap/phase/external-integration/milestones/mattermost-nexo-messaging-alignment.md`에서 먼저 닫고, 이후 `[mattermost-adapter]`는 core의 Mattermost REST 메시지 발송 구현에 집중한다.
- 현재 반영 근거:
- `services/core/internal/adapters/openai/client.go`는 non-streaming `POST /v1/responses` 호출 경로를 사용한다.
- `services/core/cmd/server/main.go``MODEL_BASE_URL`, `MODEL_API_KEY`, `MODEL_NAME`, `MODEL_CONTEXT_SIZE`, `MODEL_TIMEOUT_SEC` 설정으로 OpenAI-compatible model client를 구성해 scheduler에 연결한다.

View file

@ -0,0 +1,62 @@
# Milestone: Mattermost Nexo Messaging Alignment
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/external-integration/PHASE.md`
## 목표
현재 workflow/core 작업을 계속 확장하기 전에 Mattermost 메시지/알림 경계를 `../nexo/packages/messaging_flutter` 기준으로 먼저 고정한다. NomadCode core는 Mattermost REST 메시지 발송과 task notification 발행을 담당하고, nexo messaging package는 client-side FCM 수신, signature 검증, ACK, notification display, opened-routing, inline reply, dismiss를 담당한다는 선을 구현과 검증 문서에 맞춘다.
## 상태
[진행중]
## 구현 잠금
- 상태: 해제
- 결정 필요: 없음
## 범위
- NomadCode host의 `MattermostPushClient` 경계를 현재 `nexo_messaging` public API와 맞춘다.
- ignored Mattermost credential asset에서 optional server identifier를 전달할 수 있게 한다.
- server-generated signed push smoke 절차와 External Integration 검증 기준 nexo 구조에 맞춰 유지한다.
- Android native push 처리 로직이 `apps/client/android`로 되돌아오지 않도록 책임 경계를 문서화한다.
## 기능
### Epic: [nexo-boundary] Mattermost host and nexo package alignment
현재 작업보다 먼저 Mattermost messaging/notification 경계를 nexo-owned package 기준으로 정렬한다.
- [x] [host-identifier] NomadCode host가 optional Mattermost server identifier를 `MattermostPushClient``nexo_messaging``setAuthToken(..., identifier:)` 경계로 전달한다. 검증: `cd apps/client && flutter test`.
- [x] [docs-boundary] `docs/test-environments.md`, `apps/client/push-notification-todo.md`, contracts note, External Integration context가 nexo package의 Android-first/stub-platform 상태와 server-generated signed push smoke 기준을 일관되게 설명한다. 검증: 문서에서 raw token/password/signing key/FCM token 값을 기록하지 않는다.
- [ ] [signed-smoke-refresh] 실제 Android runner에서 server-generated signed push smoke를 재실행해 FCM receipt, valid signature, ACK, notification display, opened event, inline reply, dismiss evidence를 redaction 상태로 남긴다. 검증: `docs/test-environments.md#mattermost-server-generated-push-smoke`의 Evidence Checklist가 PASS/PARTIAL/BLOCKED로 기록된다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: [진행중] FCM 수신, signature 검증, ACK 전송, system notification display는 PASS이나, opened event, channel navigation, inline reply, dismiss/clear, device token prefix는 실제 동작 및 접두사 증적 수집 대기 중으로 부분 완료(PARTIAL) 상태입니다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- Plane/Jira adapter 확장
- Agent Integrator 또는 A2A 도입 결정
- core Mattermost REST 메시지 발송 adapter의 전체 구현
- iOS/macOS native push runtime 구현
- Mattermost 제품 UX 또는 채널 화면 구현
## 작업 컨텍스트
- 관련 경로: `apps/client/lib/src/integrations/mattermost/`, `apps/client/push-notification-todo.md`, `docs/test-environments.md`, `packages/contracts/notes/flutter-core-api-candidates.md`, `../nexo/packages/messaging_flutter`
- 표준선(선택): `../nexo/packages/messaging_flutter`는 Android-first client-side messaging/notification SDK이고, NomadCode host는 Firebase 설정, credential handoff, signing key, optional server identifier, navigation callback만 전달한다.
- 선행 작업: Mattermost Messaging Android Smoke
- 후속 작업: Workflow Core 재개, External Integration의 `[mattermost-adapter]` 구현
- 확인 필요: signed push smoke 재실행 시 private input은 `agent-ops/rules/private/rules.md`가 라우팅하는 ignored secret 파일에서만 읽는다.

View file

@ -10,6 +10,7 @@ class MattermostAuthService {
final MattermostPushClient _pushService;
String? _serverUrl;
String? _serverIdentifier;
String? _authToken;
String? _userId;
String? _sessionId;
@ -29,6 +30,7 @@ class MattermostAuthService {
return;
}
_serverUrl = creds['serverUrl']!;
_serverIdentifier = creds['serverId'];
print('[MattermostAuth] Logging in to $_serverUrl ...');
await _login(creds['loginId']!, creds['password']!);
@ -63,10 +65,12 @@ class MattermostAuthService {
return null;
}
final map = json.decode(jsonStr) as Map<String, dynamic>;
final serverId = (map['serverId'] ?? map['serverIdentifier']) as String?;
return {
'serverUrl': map['serverUrl'] as String,
'loginId': map['loginId'] as String,
'password': map['password'] as String,
if (serverId != null && serverId.isNotEmpty) 'serverId': serverId,
};
}
@ -97,7 +101,11 @@ class MattermostAuthService {
print('[MattermostAuth] Login OK - userId=$_userId sessionId=$_sessionId');
// (ACK, )
await _pushService.setAuthToken(_serverUrl!, _authToken!);
await _pushService.setAuthToken(
_serverUrl!,
_authToken!,
identifier: _serverIdentifier,
);
// config에서 signing key
await _fetchAndStoreSigningKey();

View file

@ -8,7 +8,11 @@ abstract interface class MattermostPushClient {
Future<void> initialize();
Future<String?> getDeviceToken();
Future<void> setAuthToken(String serverUrl, String token);
Future<void> setAuthToken(
String serverUrl,
String token, {
String? identifier,
});
Future<void> setSigningKey(String serverUrl, String signingKey);
set onDeviceTokenReady(Future<void> Function(String token)? callback);

View file

@ -25,8 +25,11 @@ class MattermostPushPluginClient implements MattermostPushClient {
Future<String?> getDeviceToken() => _plugin.getDeviceToken();
@override
Future<void> setAuthToken(String serverUrl, String token) =>
_plugin.setAuthToken(serverUrl, token);
Future<void> setAuthToken(
String serverUrl,
String token, {
String? identifier,
}) => _plugin.setAuthToken(serverUrl, token, identifier: identifier);
@override
Future<void> setSigningKey(String serverUrl, String signingKey) =>

View file

@ -61,9 +61,9 @@ To keep host architectures generic and lightweight, we maintain a strict boundar
- Integrates Firebase Cloud Messaging, JJWT verification, Room Database, and native background broadcasts.
### 2.3 iOS Target Status & Native Extension
- iOS is fully integrated into the unified Dart API boundary.
- **Dart Level Integration**: Sibling systems can target iOS using identical calls (`MattermostPushClient` methods).
- **Native Implementation Extension**: Sibling repos can implement native APNS delegate receivers inside the iOS project folder without needing to modify common Dart facades or common bootstrap parameters.
- iOS and macOS share the unified Dart API boundary, but `../nexo/packages/messaging_flutter` currently exposes native runtime stubs for these platforms.
- **Dart Level Integration**: Sibling systems can keep the same host-facing calls (`MattermostPushClient` methods).
- **Native Implementation Extension**: Production APNS/macOS notification behavior must be implemented in the plugin native platform folders before these targets are treated as runtime-ready.
---

View file

@ -32,7 +32,11 @@ class _FakePushClient implements MattermostPushClient {
Future<String?> getDeviceToken() async => null;
@override
Future<void> setAuthToken(String serverUrl, String token) async {}
Future<void> setAuthToken(
String serverUrl,
String token, {
String? identifier,
}) async {}
@override
Future<void> setSigningKey(String serverUrl, String signingKey) async {}

View file

@ -59,6 +59,171 @@ flutter run -d <device-id>
For Mattermost push work, confirm the app boots, Firebase initialization does not crash, and notification navigation callbacks remain observable. Do not record tokens, credentials, signing keys, or private server values in roadmap or docs.
## Mattermost Server-Generated Push Smoke
Use this when NomadCode needs to reproduce the same signed FCM path that was verified from the sibling `../nexo` messaging plugin environment.
### Roadmap Target
The current matching roadmap target is:
- Phase: `agent-roadmap/phase/external-integration/PHASE.md`
- Milestone: `agent-roadmap/phase/external-integration/milestones/external-integration.md`
- Task fit: `[mattermost-adapter]` and Mattermost notification evidence for external integration validation.
### Private Inputs
Before running this smoke, read `agent-ops/rules/private/rules.md` and use the local-only secret file referenced there. Do not print or commit raw bot tokens, passwords, FCM tokens, signing keys, or push signatures.
Create the ignored credential asset used by `apps/client`:
```json
{
"serverUrl": "<mattermost-base-url-from-private-rules>",
"loginId": "<receiver-test-account-email-from-private-rules>",
"password": "<receiver-test-account-password-from-private-rules>"
}
```
Write it to:
```bash
apps/client/assets/mattermost_credentials.json
```
This asset is intentionally ignored by `apps/client/.gitignore`. It lets `MattermostAuthService` login, call `setAuthToken`, fetch the server signing public key, call `setSigningKey`, and register the current FCM device token.
If the private environment provides a Mattermost server identifier that appears in FCM payloads as `server_id`, add it as `serverId` or `serverIdentifier` in the ignored credential asset. The host passes it through `setAuthToken(..., identifier:)` so `nexo_messaging` can resolve `server_url` even when more than one server is registered.
### Workspace Shape
The remote Android runner needs the same sibling workspace layout as the local agent workspace:
```text
<workspace>/
nomadcode/
nexo/
proto-socket/
```
`apps/client/pubspec.yaml` depends on:
```yaml
nexo_messaging:
path: ../../../nexo/packages/messaging_flutter
proto_socket:
path: ../../../proto-socket/dart
```
Make sure the remote `../nexo` checkout contains the current `packages/messaging_flutter` changes, especially the Android opened-intent fix that converts nested `Bundle` extras into Flutter codec-safe values. Without that fix, tapping a notification can crash with `Unsupported value: 'Bundle[EMPTY_PARCEL]'`.
NomadCode already uses Android `applicationId = "com.tokilabs.mattermost"` in `apps/client/android/app/build.gradle.kts`; unlike the standalone Nexo test harness, no Gradle application-id override is expected.
### Run App
On the Android remote runner:
```bash
cd <workspace>/nomadcode/apps/client
flutter pub get
flutter analyze --no-fatal-infos
flutter test
flutter run -d emulator-5554 --no-resident
```
Expected app-side setup logs include:
```text
FirebaseApp initialization successful
[MattermostAuth] Logging in to ...
[MattermostAuth] Login OK ...
[MattermostAuth] Signing key stored.
[MattermostAuth] FCM device token registered successfully.
```
If login or signing-key storage is skipped, fix the ignored credential asset or private environment before triggering the push. A valid server-generated push should not require manually injecting server URL rows or signing keys.
### Trigger Signed Push
Use the bot token from private rules to send a normal Mattermost DM or mention to the receiver test account through the Mattermost REST API. The important property is that the message is created by the server, then delivered through push-proxy to FCM; do not use a hand-built placeholder signature as the positive test.
The generic API flow is:
```bash
# 1. Resolve bot user and receiver user.
GET /api/v4/users/me
GET /api/v4/users/email/<receiver-email>
# 2. Open or create a direct channel.
POST /api/v4/channels/direct
# body: ["<bot-user-id>", "<receiver-user-id>"]
# 3. Post the smoke message.
POST /api/v4/posts
# body: {"channel_id":"<direct-channel-id>","message":"nomadcode signed push smoke <timestamp>"}
```
All calls use:
```text
Authorization: Bearer <bot-access-token-from-private-rules>
```
### Expected Evidence
Collect redacted evidence only. Good evidence is logcat snippets, sanitized UI text, or `dumpsys notification` excerpts that omit raw payload signatures and tokens.
The positive signed path should show:
```text
NexoFCM : >>> onMessageReceived ENTER
NexoFCM : onMessageReceived type=message ... serverUrl=<mattermost-base-url>
ReceiptDelivery: Send ACK=...
NexoFCM : Sending notification event to Flutter: type=message
```
It should not show:
```text
Cannot verify
No signing key
Notification skipped: signature verification failed
```
Then verify:
- System notification displays the server-generated message and has a `Reply` action.
- Tapping the notification invokes the host channel navigation callback.
- Inline reply sends successfully. NomadCode should be able to test this fully because `MattermostAuthService` calls `setAuthToken`; if reply fails, inspect auth-token handoff before treating plugin behavior as failed.
- Dismiss/clear path logs the dismiss or cancel event.
- Device registration uses the `android_rn-v2:` prefix before the raw FCM token is registered with Mattermost.
### Negative Controls
If direct push-proxy access is available, send one placeholder-signature payload and one unsigned payload as negative controls. Both may reach `onMessageReceived` and ACK, but must stop before notification display with:
```text
Notification skipped: signature verification failed
```
### Evidence Checklist
Record these outcomes in the relevant `agent-task` review stub or milestone evidence. Use `PASS`, `PARTIAL`, `BLOCKED`, or `N/A` rather than overstating results.
| 항목 | 기대 |
|------|------|
| FCM payload wakes / receipt | PASS |
| valid server-generated signature 처리 | PASS |
| invalid / unsigned drop | PASS |
| notification display | PASS |
| ACK request | PASS |
| opened event | PASS |
| channel navigation | PASS |
| thread navigation | N/A unless the payload has `root_id` |
| inline reply | PASS expected for NomadCode host auth wiring |
| dismiss / clear | PASS |
| device token prefix | PASS |
### Roadmap Evidence
When a milestone depends on this environment, leave only a short evidence note in the milestone, for example:

View file

@ -218,6 +218,7 @@
- **설명**: Mattermost push 통합에서 `apps/client` host가 plugin adapter에게 위임/공급해야 하는 데이터와 콜백. plugin adapter는 platform 측 plugin singleton에 격리되어 있고, 그 외 코드는 모두 `MattermostPushClient` 인터페이스에만 의존한다.
- **호스트 소유 필드/콜백**:
- `server_url`: Mattermost 서버 base URL (auth 핸드오프, signing key 저장에 함께 사용)
- `server_identifier`: 선택 필드. FCM payload의 `server_id``server_url`로 역해석해야 하는 multi-server host에서 plugin의 `setAuthToken(..., identifier:)`로 함께 전달
- `auth_token`: 로그인 응답에서 받은 인증 토큰 (plugin의 `setAuthToken`으로 전달)
- `signing_key`: 서버 config의 `AsymmetricSigningPublicKey` (plugin의 `setSigningKey`로 전달)
- `device_token`: FCM 디바이스 토큰 (plugin의 `getDeviceToken``onDeviceTokenReady` 콜백으로 동기화)