# 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.
- Internal AI Workspace entitlement endpoint issues short-lived run tokens and returns a dynamic Ops MCP server profile for the Platform run profile.
- 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.
- A separate `/ontology-mcp` endpoint exposes the read-only Ontology MCP with a
  different token purpose; the npm installer configures both servers independently.
- The Ops tool surface includes full issue reading (comments and attachment
  metadata), workspace-gated project creation, and bounded base64 attachment upload.
- 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/<agent-id>/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/<owner-user-id>/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.

Issue an AI Workspace entitlement for a Codex run:

```bash
curl -X POST http://127.0.0.1:4100/api/internal/v1/ai-workspace/entitlements \
  -H "Authorization: Bearer $NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "schemaVersion":"ai-workspace.entitlement-request.v1",
    "appId":"ops",
    "owner":{"userId":"local-user","email":"local@example.test"},
    "runContext":{"ops":{"workspaceSlug":"nodedc","projectId":"<project-id>"}}
  }'
```

The response uses the AI Workspace adapter contract and returns `appGrants.ops.mcpServers[]` with a bearer token narrowed to the matching active agent grant(s) for the requested Ops workspace/project. The token expires after `NODEDC_AI_WORKSPACE_RUN_TOKEN_TTL_SECONDS` seconds, is marked as `grant_scope=token`, and the response reports `grantMode=token-scoped-run-grants`. Existing long-lived agent tokens keep the legacy `grant_scope=agent` behavior for manually installed local Codex clients.

Run the same check without issuing a token:

```bash
curl -X POST http://127.0.0.1:4100/api/internal/v1/ai-workspace/preflight \
  -H "Authorization: Bearer $NODEDC_AGENT_GATEWAY_INTERNAL_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "schemaVersion":"ai-workspace.entitlement-request.v1",
    "appId":"ops",
    "owner":{"userId":"local-user","email":"local@example.test"},
    "runContext":{"ops":{"workspaceSlug":"nodedc","projectId":"<project-id>"}}
  }'
```

Preflight returns redacted diagnostics: selected agent, matching grants, scopes, `grantMode`, token TTL, and MCP endpoint metadata. It does not create or return a bearer token.

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 `<agent-token>` 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' \
  -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='<project-id>' \
npm run smoke:mcp:e2e
```

Current Tasker internal adapter contract expected by Gateway:

- `POST /api/internal/nodedc/agent/projects/resolve`
- `POST /api/internal/nodedc/agent/projects`
- `GET /api/internal/nodedc/agent/projects/:projectId/context`
- `GET /api/internal/nodedc/agent/issues?project_id=...`
- `POST /api/internal/nodedc/agent/issues`
- `GET /api/internal/nodedc/agent/issues/:issueId`
- `PATCH /api/internal/nodedc/agent/issues/:issueId`
- `POST /api/internal/nodedc/agent/issues/:issueId/attachments`
- `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`
