Nexmoot Docs

Źródło: /root/Documents/Obsidian Vault/Hindsight/Nexmoot - Projekt.md

Nexmoot

Nexmoot to control plane dla zewnętrznych agentów AI i automatyzacji: system do bezpiecznego przyjmowania propozycji, rozbijania ich na zadania, przydzielania pracy agentom, zbierania wyników, głosowania, podejmowania decyzji według deterministycznej policy oraz zapewnienia pełnego audytu.

1. Krótka definicja produktu

Nexmoot nie jest runtime'em LLM. Nie uruchamia modeli, nie orkiestruje promptów i nie próbuje być kolejnym frameworkiem agentowym.

Nexmoot jest warstwą kontroli, governance i audytu nad agentami zewnętrznymi.

System odpowiada na pytania:

2. One-liner

Nexmoot: audited governance layer for autonomous and semi-autonomous agents.

Po polsku:

Nexmoot: audytowalna warstwa decyzyjna i kontrolna dla zewnętrznych agentów AI.

3. Długoterminowa wizja

Długoterminowo Nexmoot może stać się warstwą koordynacji dla zespołów, które używają wielu agentów AI, botów, automatyzacji CI/CD i ludzi-recenzentów.

Docelowo system może obsługiwać:

  1. Agent governance

Zarządzanie uprawnieniami agentów, dozwolonymi akcjami, ścieżkami plików, integracjami i limitem autonomii.

  1. Multi-agent task market

Proposal może generować taski, które różni agenci mogą claimować, wykonywać i submitować.

  1. Council / review layer

Ludzie, agenci albo hybrydowe rady mogą głosować nad wynikiem pracy.

  1. Policy engine

Decyzje są podejmowane przez deterministyczne funkcje z pełnymi reasons[], wersjonowaniem policy i audytem.

  1. Safe external writes

Integracje z GitHubem, Linear, Jira, Notion, Slack/Discord, CI/CD, ale przez bezpieczną warstwę protected paths, kill switch i audyt.

  1. Agent reputation

System może budować reputację agentów na podstawie jakości submissionów, odsetka zaakceptowanych prac, czasu wykonania i incydentów.

  1. Compliance / audit dashboard

Pełna historia tego, co zaszło: kto, kiedy, dlaczego, według jakiej policy, z jakim wynikiem.

  1. Enterprise controls

Tenant isolation, SSO, RBAC/ABAC, signed approvals, export audit logs, legal hold.

4. Zasada strategiczna

Najważniejsza decyzja architektoniczna:

Nexmoot jest control plane, nie runtime LLM.

To oznacza:

5. Główne encje domenowe

Proposal

Proposal to propozycja zmiany, pracy, eksperymentu albo inicjatywy.

Przykłady:

Task

Task to konkretna jednostka pracy wynikająca z Proposal.

Task może być claimowany przez agenta albo człowieka.

Submission

Submission to wynik pracy przesłany przez claim ownera.

Może zawierać:

Vote

Vote to głos reviewera/council membera nad Submission.

Decision

Decision to finalny wynik procesu: accept, reject, request changes, reopen, no-op.

AuditEvent

AuditEvent to nieusuwalny zapis tego, co zaszło w systemie.

6. MVP cutline

MVP powinno być ograniczone i demonstracyjne, ale kompletne end-to-end.

W MVP robimy

W MVP nie robimy

7. Golden path demo

Najważniejszy przepływ demonstracyjny:

1. Admin/proposer tworzy Proposal.
2. Proposal zostaje zaakceptowany.
3. System publikuje Task.
4. Agent claimuje Task.
5. Agent submituje wynik.
6. Reviewer/Council głosuje.
7. Policy podejmuje decyzję.
8. Task zostaje completed albo reopened.
9. Dashboard pokazuje pełną oś czasu audytu.
10. Kill switch może zatrzymać mutujące operacje.

To demo pokazuje wartość produktu bez „magii” i bez udawania, że system samodzielnie rozwiązuje wszystkie problemy agentów.

8. State machine

Status enumy

type ProposalStatus =
  | "draft"
  | "submitted"
  | "under_review"
  | "approved"
  | "rejected"
  | "cancelled"
  | "archived";

type TaskStatus =
  | "draft"
  | "open"
  | "claimed"
  | "submitted"
  | "in_review"
  | "completed"
  | "cancelled";

type SubmissionStatus =
  | "draft"
  | "submitted"
  | "under_review"
  | "changes_requested"
  | "accepted"
  | "rejected"
  | "withdrawn";

expired dla claimów lepiej traktować jako zdarzenie audytowe, niekoniecznie trwały status Taska.

Jedna tabela przejść

EntityFromToActionActorWarunki
Proposaldraftsubmittedproposal.submitproposerProposal ma wymagane pola
Proposalsubmittedunder_reviewproposal.start_reviewadmin, systemProposal oczekuje na ocenę
Proposalunder_reviewapprovedproposal.approveadminSpełnia kryteria
Proposalunder_reviewrejectedproposal.rejectadminWymagany powód
Proposaldraftcancelledproposal.cancelproposer, adminBrak aktywnych Tasków
Proposalsubmittedcancelledproposal.cancelproposer, adminBrak aktywnych Tasków
Proposalapprovedarchivedproposal.archiveadmin, systemWszystkie Taski terminalne
Proposalrejecteddraftproposal.reopenproposer, adminPoprawki dozwolone
Taskdraftopentask.publishadmin, systemParent Proposal jest approved
Taskopenclaimedtask.claimagentBrak aktywnego claimu
Taskclaimedopentask.release_claimclaim_owner, admin, systemBrak accepted Submission
Taskclaimedsubmittedtask.submitclaim_ownerClaim aktywny i niewygasły
Tasksubmittedin_reviewtask.start_reviewsystem, adminIstnieje Submission
Taskin_reviewcompletedtask.completesystem, adminPolicy zaakceptowała Submission
Taskin_reviewopentask.reopensystem, adminPolicy odrzuca i pozwala retry
Taskopencancelledtask.canceladminPowód wymagany
Taskclaimedcancelledtask.canceladminPowód wymagany
Tasksubmittedcancelledtask.canceladminPowód wymagany
Submissiondraftsubmittedsubmission.submitclaim_ownerTask claimnięty przez aktora
Submissionsubmittedunder_reviewsubmission.start_reviewsystem, adminPayload poprawny
Submissionunder_reviewacceptedsubmission.acceptsystem, adminPolicy zwraca accept
Submissionunder_reviewrejectedsubmission.rejectsystem, adminPolicy zwraca reject
Submissionunder_reviewchanges_requestedsubmission.request_changesreviewer, admin, systemPoprawki dozwolone
Submissionchanges_requestedsubmittedsubmission.resubmitclaim_ownerW terminie
Submissionsubmittedwithdrawnsubmission.withdrawclaim_ownerPrzed decyzją
Submissionunder_reviewwithdrawnsubmission.withdrawclaim_owner, adminPrzed decyzją
SubmissionacceptedterminalBrak dalszych przejść
SubmissionrejectedterminalBrak dalszych przejść
SubmissionwithdrawnterminalBrak dalszych przejść

9. Role i uprawnienia

Minimalne role:

type ActorRole =
  | "proposer"
  | "agent"
  | "reviewer"
  | "admin"
  | "system";

Rekomendowany model:

can(actor, action, resource): boolean

Przykładowe akcje:

"proposal.submit"
"proposal.approve"
"task.claim"
"task.submit"
"submission.vote"
"task.decide"
"task.cancel"
"system.pause"
"system.resume"

Authz i policy decyzyjna powinny być rozdzielone:

10. Idempotencja

Endpointy wymagające Idempotency-Key:

POST /tasks/:taskId/claim
POST /tasks/:taskId/submit
POST /submissions/:submissionId/vote
POST /tasks/:taskId/decide

Tabela:

CREATE TABLE idempotency_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  actor_id UUID NOT NULL,
  method TEXT NOT NULL,
  endpoint TEXT NOT NULL,
  resource_type TEXT NOT NULL,
  resource_id UUID NOT NULL,
  idempotency_key TEXT NOT NULL,
  request_hash TEXT NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('processing', 'completed', 'failed')),
  response_status INTEGER,
  response_body JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at TIMESTAMPTZ NOT NULL,
  UNIQUE (actor_id, method, endpoint, idempotency_key)
);

Zachowanie:

SytuacjaWynik
Pierwszy requestWykonaj i zapisz response
Ten sam key + ten sam bodyZwróć zapisany response
Ten sam key + inny body409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST
Brak key400 IDEMPOTENCY_KEY_REQUIRED
Request w trakcie processing409 IDEMPOTENCY_REQUEST_IN_PROGRESS albo Retry-After

11. Atomowy claim taska

Dla MVP rekomendowany jest atomowy update:

UPDATE tasks
SET
  status = 'claimed',
  claimed_by = :actor_id,
  claimed_at = now(),
  claim_expires_at = now() + interval '30 minutes'
WHERE id = :task_id
  AND status = 'open'
  AND claimed_by IS NULL
RETURNING id, status, claimed_by, claimed_at, claim_expires_at;

Jeśli RETURNING zwraca 0 rekordów, request dostaje:

{
  "error": {
    "code": "TASK_ALREADY_CLAIMED",
    "message": "Task is no longer available for claim.",
    "request_id": "req_..."
  }
}

Nie wolno robić klasycznego flow SELECT -> if open -> UPDATE bez atomowego warunku.

12. Constraints w DB

CREATE UNIQUE INDEX uniq_accepted_submission_per_task
ON submissions (task_id)
WHERE status = 'accepted';

CREATE UNIQUE INDEX uniq_vote_per_submission_voter
ON votes (submission_id, voter_id);

CREATE UNIQUE INDEX uniq_final_decision_per_task
ON decisions (task_id)
WHERE decision_type IN ('accepted', 'rejected', 'cancelled');

Jeżeli claimy są w osobnej tabeli:

CREATE UNIQUE INDEX uniq_active_claim_per_task
ON task_claims (task_id)
WHERE released_at IS NULL
  AND expired_at IS NULL;

13. Globalny format błędów

Każdy błąd ma format:

{
  "error": {
    "code": "TASK_ALREADY_CLAIMED",
    "message": "Task is no longer available for claim.",
    "details": {
      "task_id": "task-id"
    },
    "request_id": "req_123"
  }
}

Minimalne kody:

type ErrorCode =
  | "VALIDATION_ERROR"
  | "AUTHENTICATION_REQUIRED"
  | "AUTHORIZATION_DENIED"
  | "RESOURCE_NOT_FOUND"
  | "CONFLICT"
  | "INVALID_STATE_TRANSITION"
  | "IDEMPOTENCY_KEY_REQUIRED"
  | "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST"
  | "IDEMPOTENCY_REQUEST_IN_PROGRESS"
  | "TASK_ALREADY_CLAIMED"
  | "TASK_CLAIM_EXPIRED"
  | "TASK_NOT_CLAIMED_BY_ACTOR"
  | "SUBMISSION_ALREADY_EXISTS"
  | "VOTE_ALREADY_EXISTS"
  | "DECISION_ALREADY_EXISTS"
  | "POLICY_REJECTED"
  | "INTERNAL_ERROR";

Test kontraktowy:

expect(response.body).toEqual({
  error: expect.objectContaining({
    code: expect.any(String),
    message: expect.any(String),
    request_id: expect.any(String)
  })
});

expect(response.body.error).not.toHaveProperty("stack");
expect(VALID_ERROR_CODES).toContain(response.body.error.code);

14. AuditEvent schema

CREATE TABLE audit_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  actor_type TEXT NOT NULL,
  actor_id UUID,
  action TEXT NOT NULL,
  subject_type TEXT NOT NULL,
  subject_id UUID NOT NULL,
  object_type TEXT,
  object_id UUID,
  request_id TEXT,
  idempotency_key TEXT,
  metadata JSONB NOT NULL DEFAULT '{}'::jsonb
);

actor_type:

type ActorType = "user" | "agent" | "reviewer" | "admin" | "system";

subject_type:

type SubjectType =
  | "proposal"
  | "task"
  | "submission"
  | "vote"
  | "decision"
  | "policy_evaluation"
  | "idempotency_key";

Minimalny AuditAction:

type AuditAction =
  | "proposal.created"
  | "proposal.submitted"
  | "proposal.review_started"
  | "proposal.approved"
  | "proposal.rejected"
  | "proposal.cancelled"
  | "proposal.archived"
  | "proposal.reopened"
  | "task.created"
  | "task.published"
  | "task.claimed"
  | "task.claim_released"
  | "task.claim_expired"
  | "task.submitted"
  | "task.review_started"
  | "task.completed"
  | "task.reopened"
  | "task.cancelled"
  | "submission.created"
  | "submission.submitted"
  | "submission.review_started"
  | "submission.accepted"
  | "submission.rejected"
  | "submission.changes_requested"
  | "submission.withdrawn"
  | "submission.resubmitted"
  | "vote.created"
  | "vote.updated"
  | "vote.deleted"
  | "decision.created"
  | "decision.applied"
  | "policy.evaluated"
  | "policy.accepted"
  | "policy.rejected"
  | "idempotency.started"
  | "idempotency.replayed"
  | "idempotency.conflict";

Każda zmiana stanu zapisuje w metadata:

{
  "from_status": "open",
  "to_status": "claimed",
  "reason": "agent_claimed_task"
}

15. Policy jako deterministyczna funkcja

Kontrakt:

function evaluatePolicy(input: PolicyInput): PolicyResult

Policy nie może:

Input:

type PolicyInput = {
  now: string;
  policy_name: string;
  policy_version: string;
  task: {
    id: string;
    status: TaskStatus;
    claimed_by?: string;
    claimed_at?: string;
    claim_expires_at?: string;
  };
  submission?: {
    id: string;
    status: SubmissionStatus;
    submitted_by: string;
    submitted_at: string;
  };
  votes: Array<{
    voter_id: string;
    value: "approve" | "reject" | "abstain";
    weight: number;
    created_at: string;
  }>;
  config: {
    quorum: number;
    min_approvals: number;
    min_rejections_to_fail: number;
    allow_self_vote: boolean;
    voting_deadline?: string;
  };
};

Output:

type PolicyResult = {
  decision:
    | "accept"
    | "reject"
    | "request_changes"
    | "keep_reviewing"
    | "reopen_task"
    | "no_op";
  reasons: Array<{
    code: string;
    message: string;
    facts?: Record<string, unknown>;
  }>;
  effects: Array<{
    type:
      | "set_task_status"
      | "set_submission_status"
      | "create_decision"
      | "emit_audit_event";
    payload: Record<string, unknown>;
  }>;
};

Aplikacja aplikuje effects[] dopiero w transakcji po walidacji allowed transitions.

16. Kill switch

Kill switch musi być pierwszoklasowym guardem przed mutującymi komendami.

Blokuje:

Nie blokuje:

Pseudo-flow:

assertSystemNotPaused(commandName);

AuditEvent:

system.paused
system.resumed
command.blocked_by_kill_switch

Można dodać je do enumu, jeśli kill switch jest częścią MVP dashboardu.

17. Protected paths

Fake GitHub / future GitHub adapter powinien od początku modelować protected paths.

Przykład:

type ProtectedPathRule = {
  pattern: string;
  action: "allow" | "deny" | "require_review";
  reason: string;
};

MVP rules:

DENY .env
DENY secrets/**
DENY infra/prod/**
REQUIRE_REVIEW package.json
REQUIRE_REVIEW migrations/**
ALLOW docs/**
ALLOW src/**

18. Architektura aplikacji

Rekomendowany układ logiczny:

API layer
  -> request_id
  -> authn
  -> authz
  -> kill switch guard
  -> idempotency guard
  -> command handler
      -> load aggregate
      -> validate transition
      -> execute domain operation
      -> write audit event
      -> commit transaction
  -> persist idempotent response
  -> return response

Dla decide:

POST /tasks/:taskId/decide
  -> idempotency
  -> transaction
      -> load task/submissions/votes FOR UPDATE
      -> build PolicyInput
      -> evaluatePolicy(input)
      -> validate PolicyResult.effects
      -> apply effects
      -> write Decision
      -> write AuditEvents
  -> return decision + reasons[]

19. Proponowany stack MVP

Jeśli nie ma jeszcze wybranego stacku, rekomendacja pragmatyczna:

Backend

Frontend dashboard

Alternatywa bardziej „backend-first”

Najważniejsze: stack ma wspierać szybkie testy integracyjne transakcji i constraints w PostgreSQL.

20. Plan wdrożenia MVP przez PR-y

PR 1 — Foundation

Cel: uruchomić szkielet systemu.

Zakres:

Acceptance criteria:

PR 2 — Audit core

Cel: audyt jako pierwszoklasowa funkcja.

Zakres:

Acceptance criteria:

PR 3 — Identity i minimal Authz

Cel: minimalny model aktorów i uprawnień.

Zakres:

Acceptance criteria:

PR 4 — State machine core

Cel: centralny transition validator.

Zakres:

Acceptance criteria:

PR 5 — Proposal / Task / Submission models

Cel: podstawowy model domenowy.

Zakres:

Acceptance criteria:

PR 6 — Idempotency layer

Cel: ochrona endpointów krytycznych.

Zakres:

Acceptance criteria:

PR 7 — Task claim

Cel: bezpieczny claim.

Zakres:

Acceptance criteria:

PR 8 — Submit

Cel: claim owner może wysłać wynik.

Zakres:

Acceptance criteria:

PR 9 — Vote

Cel: reviewer/council może głosować.

Zakres:

Acceptance criteria:

PR 10 — Policy + Decide

Cel: deterministyczna decyzja.

Zakres:

Acceptance criteria:

PR 11 — Kill switch + protected paths

Cel: podstawowe bezpieczeństwo operacyjne.

Zakres:

Acceptance criteria:

PR 12 — Dashboard MVP

Cel: czytelny podgląd systemu.

Zakres:

Acceptance criteria:

PR 13 — Golden path demo

Cel: finalne demo MVP.

Zakres:

Acceptance criteria:

21. Sprinty

Sprint 0 — Decyzje i repo

Sprint 1 — Foundation + Audit

PR 1 + PR 2.

Cel: wszystko, co stanie się później, ma request_id i audyt.

Sprint 2 — Identity + State Machine

PR 3 + PR 4.

Cel: zanim powstaną endpointy mutujące, istnieją centralne reguły uprawnień i przejść.

Sprint 3 — Core Domain + Idempotency

PR 5 + PR 6.

Cel: modele domenowe i bezpieczny retry layer.

Sprint 4 — Claim/Submit/Vote

PR 7 + PR 8 + PR 9.

Cel: agenci mogą wykonać realny lifecycle pracy.

Sprint 5 — Policy/Decide/Safety

PR 10 + PR 11.

Cel: decyzje, kill switch i protected paths.

Sprint 6 — Dashboard + Demo

PR 12 + PR 13.

Cel: gotowe MVP demonstracyjne.

22. Najważniejsze testy

Contract tests

State machine tests

Concurrency tests

Policy tests

E2E tests

23. Rzeczy, na które trzeba uważać

1. Nie rozpraszać logiki domenowej po endpointach

Endpointy powinny być cienkie. Logika ma mieszkać w command handlers, transition validatorze i policy.

2. Nie mieszać authz z policy

Authz: czy aktor może wykonać akcję.

Policy: jaka decyzja wynika z danych.

3. Nie ufać idempotency bez constraints

Idempotency-Key to warstwa API. Integralność musi być też wymuszona przez DB.

4. Nie robić claim przez SELECT potem UPDATE

Claim musi być atomowy.

5. Nie robić policy z side effectami

Policy musi być testowalna i deterministyczna.

6. Nie traktować audytu jako dodatku

Audyt jest częścią produktu, nie logiem developerskim.

7. Nie rozbudowywać MVP przed demo

Największe ryzyko produktowe to scope creep.

8. Nie udawać real GitHuba za wcześnie

Fake GitHub w MVP jest zaletą: pozwala przetestować governance bez ryzyka realnych write'ów.

9. Nie ignorować processing idempotency

Najtrudniejsze bugi pojawią się przy równoległych retry.

10. Nie zostawiać enumów jako luźnych stringów

Statusy, action, subject_type, decision_type i error_code powinny być kontrolowane.

24. Metryki sukcesu MVP

MVP jest udane, jeśli:

25. Potencjalne rozszerzenia po MVP

V1

V2

V3

26. Krótka ocena projektu

Aktualna ocena:

Wizja produktu: 9/10
Realność MVP: 9/10
Bezpieczeństwo-by-design: 9/10
Gotowość do implementacji: 8.5/10
Ryzyko implementacyjne: umiarkowane, ale kontrolowane

Największy warunek sukcesu:

Nie rozproszyć logiki domenowej po endpointach.

Najlepszy tryb pracy:

mały PR -> test kontraktowy -> audyt -> demo path

27. Decyzje do podjęcia przed pierwszym commitem

  1. Stack: TypeScript/Fastify/Nest czy Python/FastAPI.
  2. Czy claim jest kolumnami na tasks, czy osobną tabelą task_claims.
  3. Czy vote jest immutable, czy updateable do deadline.
  4. Czy self-vote jest domyślnie zablokowany.
  5. Czy proposer może być reviewerem.
  6. Jak długo trzymamy idempotency keys.
  7. Czy MVP ma multi-tenant scope, czy single-tenant.
  8. Czy dashboard jest w tym samym repo, czy osobno.

Rekomendacje domyślne:

Stack: TypeScript + PostgreSQL + Fastify/Nest + Drizzle/Prisma
Claim: kolumny na tasks dla MVP
Vote: updateable do deadline
Self-vote: blocked by default
Proposer as reviewer: blocked by default for MVP
Idempotency TTL: claim 24h, submit/vote 7d, decide 30d
Tenant: single-tenant MVP
Dashboard: same repo for speed

28. Podsumowanie

Nexmoot powinien być budowany jako mały, twardy rdzeń domenowy, a nie szeroka platforma od pierwszego dnia.

MVP ma dowieźć jedną rzecz bardzo dobrze:

Bezpieczny, audytowalny lifecycle pracy zewnętrznego agenta:
Proposal -> Task -> Claim -> Submission -> Vote -> Policy Decision -> Audit Dashboard.

Jeżeli ten przepływ będzie działał deterministycznie, idempotentnie, z pełnym audytem i bez race conditions, projekt będzie miał bardzo mocny fundament pod dalszą rozbudowę.