# NODE.DC Tasker Codex API Отдельный модуль NODE.DC для безопасного подключения локальных Codex/AI-агентов к Tasker / Operational Core. Модуль не является частью Plane fork и не должен становиться backend-расширением Tasker. Его роль — agent gateway: выдача ограниченных agent credentials, проверка прав, MCP/REST-контракт для внешних агентов, аудит и маршрутизация разрешённых операций в Tasker через узкий internal adapter. ## Documents - [Architecture](docs/ARCHITECTURE.md) - [UX flow](docs/UX_FLOW.md) - [MCP tools contract](docs/MCP_TOOLS_CONTRACT.md) - [Tasker API audit](docs/TASKER_API_AUDIT.md) - [Threat model](docs/THREAT_MODEL.md) - [Implementation plan](docs/IMPLEMENTATION_PLAN.md) ## Core rule External Codex instances never receive Plane session cookies, raw Tasker API tokens, database access, or a generic HTTP proxy into Tasker. All writes go through NODE.DC Agent Gateway, are scoped by agent grants, and are recorded as actions of a dedicated agent identity owned by a human platform user. ## Current implementation - Fastify service with `/healthz`, `/readyz`, and capability metadata. - Postgres migrations for agents, grants, token hashes, pairing codes, audit events, and idempotency keys. - Internal REST endpoints for agent profile, grant, and token lifecycle. - Lifecycle endpoints are protected by `NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN`; public agent traffic uses only issued agent tokens. - Opaque agent tokens are generated once and stored only as SHA-256 hashes. - Authenticated agent-session endpoint returns effective grants/scopes for future MCP calls. - Agent setup endpoint returns an MCP config template and AGENTS.md instruction pack without echoing the raw token. - Product tool endpoints validate agent token, scopes, and project grants before calling Tasker internal adapter. - MCP JSON-RPC endpoint `/mcp` exposes the same tool runtime as REST product endpoints. - Write tools require idempotency keys and replay successful duplicate requests without creating duplicate Tasker writes. - Agent Gateway writes audit events for executed, replayed, and failed write-tool calls. - Tool execution calls the real Tasker internal adapter; no fake Tasker storage exists in Gateway. - Local real e2e smoke verifies Gateway -> MCP -> Tasker runtime writes. ## Local development Product-like Docker run: ```bash cp .env.example .env docker compose --env-file .env -f docker-compose.local.yml up -d --build curl http://127.0.0.1:4100/readyz ``` The `agent-gateway` container waits for local Postgres, runs migrations on startup, and exposes the same `:4100` internal endpoint used by Tasker (`PLANE_NODEDC_AGENT_GATEWAY_URL=http://host.docker.internal:4100` in local development). `HOST_BIND` and `HOST_PORT` control the host-side port for reverse proxy deployments; Synology should use `docker-compose.synology.yml` with `172.22.0.222:18190:4100` because `18090` is reserved for Tasker. The user-facing setup packet uses `NODEDC_AGENT_GATEWAY_PUBLIC_URL`; product defaults point to `https://ops-agents.nodedc.ru`, not localhost. Synology deployment notes live in `docs/SYNOLOGY_DEPLOY.md`. Direct Node.js development: ```bash cp .env.example .env docker compose --env-file .env -f docker-compose.local.yml up -d postgres npm install npm run migrate npm run dev ``` Useful checks: ```bash npm run check npm run build npm run smoke:mcp npm run smoke:gateway curl http://127.0.0.1:4100/readyz curl http://127.0.0.1:4100/api/v1/meta/capabilities ``` Create a local test agent: ```bash curl -X POST http://127.0.0.1:4100/api/v1/agents \ -H "Authorization: Bearer $NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"owner_user_id":"local-user","owner_email":"local@example.test","display_name":"Local Codex"}' ``` Create a token and inspect effective agent session: ```bash TOKEN=$(curl -sS -X POST http://127.0.0.1:4100/api/v1/agents//tokens \ -H "Authorization: Bearer $NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"Local Codex token"}' | jq -r .token) curl http://127.0.0.1:4100/api/v1/agent-session \ -H "Authorization: Bearer $TOKEN" ``` Tasker UI should use the owner-scoped internal lifecycle API through its backend proxy: ```bash curl http://127.0.0.1:4100/api/internal/v1/owners//agents \ -H "Authorization: Bearer $NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN" ``` The internal API verifies the owner path against the stored agent owner before returning agent detail, grants, tokens, setup packets, or revoke responses. Generate a local Codex setup packet: ```bash curl http://127.0.0.1:4100/api/v1/agent-session/setup \ -H "Authorization: Bearer $TOKEN" | jq -r .setup.agents_md ``` The setup packet includes: - MCP endpoint and header template with `` placeholder; - available tools for the current grants; - AGENTS.md rules for Tasker card writing, no-delete boundary, and required `idempotency_key`; - no raw token echo. Call MCP tools through the JSON-RPC endpoint: ```bash curl -sS http://127.0.0.1:4100/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2025-06-18' \ -H "Authorization: Bearer $TOKEN" \ -d '{"jsonrpc":"2.0","id":"tools","method":"tools/list","params":{}}' | jq ``` ## Local testing strategy No fake Tasker storage is embedded into Agent Gateway. Local verification is split into product layers: 1. `npm run smoke:mcp` verifies MCP initialize, tool listing, bearer token auth, scope checks, grant checks, idempotency requirement, and the Tasker boundary. 2. `npm run smoke:gateway` verifies the REST compatibility boundary and idempotency requirement over the same tool execution path. 3. `npm run smoke:e2e` verifies REST tool endpoints and idempotent replay against the real local Tasker runtime. 4. `npm run smoke:mcp:e2e` verifies MCP tool calls and idempotent replay against the real local Tasker runtime. 5. External-machine testing uses the same token and endpoint shape against staging HTTPS; no extra protocol or fake environment should be introduced. Example real localhost MCP e2e: ```bash TOKEN=$(python3 - <<'PY' from pathlib import Path for line in Path('/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-app/plane.env').read_text().splitlines(): if line.startswith('NODEDC_INTERNAL_ACCESS_TOKEN=') or line.startswith('PLANE_NODEDC_ACCESS_TOKEN='): value = line.split('=', 1)[1].strip().strip('"').strip("'") if value: print(value) break PY ) DATABASE_URL='postgres://nodedc_agent_gateway:replace-with-local-postgres-password@localhost:54100/nodedc_agent_gateway' \ NODE_ENV=development \ LOG_LEVEL=silent \ NODEDC_TASKER_INTERNAL_URL='http://localhost:8090' \ NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN='replace-with-gateway-internal-token' \ NODEDC_INTERNAL_ACCESS_TOKEN="$TOKEN" \ SMOKE_WORKSPACE_SLUG='nodedc' \ SMOKE_PROJECT_ID='' \ npm run smoke:mcp:e2e ``` Current Tasker internal adapter contract expected by Gateway: - `POST /api/internal/nodedc/agent/projects/resolve` - `GET /api/internal/nodedc/agent/projects/:projectId/context` - `GET /api/internal/nodedc/agent/issues?project_id=...` - `POST /api/internal/nodedc/agent/issues` - `PATCH /api/internal/nodedc/agent/issues/:issueId` - `POST /api/internal/nodedc/agent/issues/:issueId/move` - `POST /api/internal/nodedc/agent/issues/:issueId/comments` - `PUT /api/internal/nodedc/agent/issues/:issueId/labels` - `PUT /api/internal/nodedc/agent/issues/:issueId/assignees`