Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,31 @@ API_HOST=0.0.0.0
API_PORT=8000
API_ENV=development

# Secret used to sign session tokens — change this in production
# Secret used to sign session tokens — MUST change in production.
# Generate with: python -c "import secrets; print(secrets.token_urlsafe(48))"
API_SECRET_KEY=replace-with-a-long-random-secret

# Shared operator API key — all operators present this key to log in.
# MUST change in production.
# Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"
OPERATOR_API_KEY=replace-with-operator-key

# Operator session token lifetime (hours). Default: 8
# OPERATOR_TOKEN_EXPIRE_HOURS=8

# Auth posture for read endpoints.
# true (default/production): all list/detail endpoints require operator JWT.
# false (demo mode): endpoints are publicly readable without auth.
# Set to false for open demo setups or local development.
REQUIRE_AUTH_FOR_READS=false

# ─────────────────────────────────────────────
# Database (apps/api)
# ─────────────────────────────────────────────
DATABASE_URL=postgresql://agentlink:agentlink@localhost:5432/agentlink_dev
# Use the async driver matching your database:
# PostgreSQL: postgresql+asyncpg://user:pass@host:5432/dbname
# SQLite (dev only): sqlite+aiosqlite:///./agentlink.db
DATABASE_URL=postgresql+asyncpg://agentlink:agentlink@localhost:5432/agentlink_dev

# ─────────────────────────────────────────────
# Redis (presence, queues, transient state)
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ out/
.env.local
.env.*.local
!.env.example
.claude/

# Python
__pycache__/
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# - Docker + Docker Compose

.PHONY: help install build dev typecheck lint test coverage validate release-gate clean \
api-install api-dev api-migrate \
api-install api-dev api-migrate api-seed-demo \
api-test api-coverage \
docker-up docker-down docker-build \
bootstrap
Expand All @@ -31,7 +31,9 @@ help:
@echo " clean Remove build artifacts and node_modules"
@echo ""
@echo " api-install Install Python deps for apps/api"
@echo " api-migrate Apply Alembic migrations (alembic upgrade head)"
@echo " api-dev Start FastAPI dev server"
@echo " api-seed-demo Seed local demo data (nodes, agents, tasks, audit events)"
@echo " api-test Run Python API tests (pytest with coverage)"
@echo " api-coverage Alias for api-test (coverage always included)"
@echo ""
Expand Down Expand Up @@ -84,6 +86,12 @@ clean:
api-install:
cd apps/api && pip install -r requirements.txt

api-migrate:
cd apps/api && alembic upgrade head

api-seed-demo:
cd apps/api && python scripts/seed_demo.py

api-test:
cd apps/api && python -m pytest

Expand Down
94 changes: 75 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ AgentLink is a platform for linking AI agent ecosystems across devices and runti

## Current status

**Pre-release MVP. Not yet stable for production use.**
**Multi-user MVP. Open-source release candidate.**

- The core server, web UI, node runtime, and protocol types are implemented and tested.
- CI and the release gate pass end-to-end.
- Some architecture documentation describes the target direction, not just the implemented state.
- The public stable release is not yet cut. Expect breaking changes.
What works today:

- Multi-user organizations with RBAC (owner/admin/operator/viewer)
- Invite-based onboarding with short-lived tokens
- Trust link foundation for peer-to-peer node relationships
- Node enrollment, heartbeat, and capability discovery
- Task creation, routing, policy evaluation, and approval gating
- Artifact storage (inline and file-backed) with retention policies
- Immutable audit trail with keyset pagination
- Operator dashboard with real-time node activity and task lifecycle views
- Retry semantics with exponential backoff (automatic and manual)
- WebSocket dispatch channel for live task notifications

The public stable release is not yet cut. Expect breaking changes.

---

Expand Down Expand Up @@ -47,13 +57,15 @@ The initial focus is **trusted-circle collaboration**: two or more users securel
| `scripts/release-gate.js` | Full-stack validation script (single source of CI truth) |
| `.github/workflows/ci.yml` | CI pipeline |

**Not yet present (planned):**
**Planned (not yet present):**

- `packages/sdk-js/` and `packages/sdk-python/` — public SDKs
- `adapters/generic-websocket/` — WebSocket adapter
- `apps/desktop/` — optional desktop shell
- `services/relay/` — relay service for NAT traversal
- Multi-org and team management features
- Cross-peer task routing and federation
- Per-node capability-aware routing
- Production deployment guides

---

Expand Down Expand Up @@ -153,27 +165,57 @@ Full type definitions live in `packages/protocol/src/`.

## Getting started

**Prerequisites:** Node >= 20, pnpm >= 9, Python >= 3.11.
**Prerequisites:** Node >= 20, pnpm >= 9, Python >= 3.11, Docker (for PostgreSQL).

### Quick start

```bash
# Clone and install JS/TS dependencies
# 1. Clone and install
git clone https://github.com/jakesterns/AgentLink.git
cd agentlink
pnpm install

# Install Python API dependencies
pip install -r apps/api/requirements.txt
```

**Build:**
# 2. Start PostgreSQL (or use an existing instance)
docker compose -f docker-compose.dev.yml up -d postgres

```bash
pnpm run build
# 3. Apply database migrations
cd apps/api && alembic upgrade head

# 4. Seed demo data (nodes, agents, tasks, audit events)
python scripts/seed_demo.py

# 5. Start the API server
uvicorn main:app --reload --host 0.0.0.0 --port 8000 &

# 6. Start the web dashboard (from repo root)
cd ../.. && pnpm --filter @agentlink/web dev
```

**Typecheck:**
Open <http://localhost:3000> to see the dashboard with seeded demo data.

### Demo credentials

| Field | Value |
| ------------ | ---------------------------------------- |
| Operator ID | `demo` |
| API Key | `dev-operator-key-change-in-production` |

Log in at `/operator/login` to access approvals, audit log, and org management.

### Demo walkthrough

1. **Dashboard** — See enrolled nodes, active tasks, and pending approvals
2. **Nodes** — View node status (online/offline), capabilities, and queue health
3. **Submit Task** — Create a task at `/tasks/new` with type `research` and any instruction
4. **Approvals** — Approve or reject tasks requiring human approval
5. **Audit Log** — View the full audit trail of system actions
6. **Task Detail** — Click any task to see its lifecycle, artifacts, and routing decisions

### Build and typecheck

```bash
pnpm run build
pnpm run typecheck
```

Expand Down Expand Up @@ -233,19 +275,33 @@ See [SECURITY.md](SECURITY.md) for the vulnerability reporting process.

---

## Multi-user and organizations

AgentLink supports multiple operators working in shared workspaces:

- **Organizations** — shared scoping for all resources (nodes, tasks, artifacts, audit events)
- **RBAC roles** — owner, admin, operator, viewer with hierarchical permissions
- **Invite flow** — admin creates a short-lived invite token; recipient accepts to join the org
- **Trust links** — directional peer trust between nodes (foundation for cross-node routing)

See [docs/auth/rbac.md](docs/auth/rbac.md) and [docs/auth/organizations.md](docs/auth/organizations.md) for details.

## Roadmap

Post-MVP directions include:

- Cross-peer task routing via trust links
- Full trust-link federation between orgs
- Public JS and Python SDKs (`packages/sdk-js/`, `packages/sdk-python/`)
- Additional adapters (LangGraph, MCP, WebSocket)
- Smarter routing (cost-aware, latency-aware, privacy-aware dispatch)
- Team and organization features
- Richer org management UI (org switcher, member management dashboard)
- Per-node capability-aware routing
- Desktop shell (`apps/desktop/`)
- Relay service for NAT traversal
- Adapter conformance test suite
- Production deployment guides

Nothing above exists yet. Major additions should be proposed via a GitHub Discussion or issue before implementation.
Major additions should be proposed via a GitHub Discussion or issue before implementation.

---

Expand Down
Binary file modified apps/api/.coverage
Binary file not shown.
19 changes: 11 additions & 8 deletions apps/api/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,24 @@ def run_migrations_offline() -> None:
context.run_migrations()


def do_run_migrations(connection) -> None:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()


async def run_migrations_online() -> None:
"""
Run migrations in 'online' mode with a live async database connection.
"""
connectable = create_async_engine(settings.database_url, echo=False)

async with connectable.connect() as connection:
await connection.run_sync(
lambda sync_conn: context.configure(
connection=sync_conn,
target_metadata=target_metadata,
compare_type=True,
)
)
await connection.run_sync(lambda _: context.run_migrations())
await connection.run_sync(do_run_migrations)

await connectable.dispose()

Expand Down
119 changes: 119 additions & 0 deletions apps/api/alembic/versions/012_multi_user_orgs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Multi-user: organizations, memberships, invites, trust links

Adds the multi-user authorization model:
- organizations table: shared workspaces scoping all resources
- org_memberships table: RBAC role bindings (owner/admin/operator/viewer)
- org_invites table: short-lived invite tokens for org onboarding
- trust_links table: directional peer trust between nodes
- org_id columns on tasks and audit_events for org scoping

Revision ID: 012
Revises: 011
Create Date: 2026-03-31 00:00:00.000000
"""
from __future__ import annotations

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

revision: str = "012"
down_revision: Union[str, None] = "011"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ── Organizations ────────────────────────────────────────────────────────
op.create_table(
"organizations",
sa.Column("org_id", sa.String(64), primary_key=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("slug", sa.String(128), nullable=False, unique=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_organizations_slug", "organizations", ["slug"], unique=True)

# ── Organization memberships ─────────────────────────────────────────────
op.create_table(
"org_memberships",
sa.Column("membership_id", sa.String(64), primary_key=True),
sa.Column(
"org_id",
sa.String(64),
sa.ForeignKey("organizations.org_id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("operator_id", sa.String(128), nullable=False),
sa.Column("role", sa.String(32), nullable=False, server_default="operator"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_org_memberships_org_id", "org_memberships", ["org_id"])
op.create_index("ix_org_memberships_operator_id", "org_memberships", ["operator_id"])
op.create_index(
"ix_org_memberships_org_operator",
"org_memberships",
["org_id", "operator_id"],
unique=True,
)

# ── Organization invites ─────────────────────────────────────────────────
op.create_table(
"org_invites",
sa.Column("invite_id", sa.String(64), primary_key=True),
sa.Column(
"org_id",
sa.String(64),
sa.ForeignKey("organizations.org_id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("invited_by", sa.String(128), nullable=False),
sa.Column("invite_token", sa.String(128), nullable=False, unique=True),
sa.Column("role", sa.String(32), nullable=False, server_default="operator"),
sa.Column("status", sa.String(32), nullable=False, server_default="pending"),
sa.Column("accepted_by", sa.String(128), nullable=True),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_org_invites_org_id", "org_invites", ["org_id"])
op.create_index("ix_org_invites_token", "org_invites", ["invite_token"], unique=True)
op.create_index("ix_org_invites_status", "org_invites", ["status"])

# ── Trust links ──────────────────────────────────────────────────────────
op.create_table(
"trust_links",
sa.Column("link_id", sa.String(64), primary_key=True),
sa.Column(
"org_id",
sa.String(64),
sa.ForeignKey("organizations.org_id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("node_a_id", sa.String(64), nullable=False),
sa.Column("node_b_id", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="active"),
sa.Column("created_by", sa.String(128), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_trust_links_org_id", "trust_links", ["org_id"])
op.create_index("ix_trust_links_node_a", "trust_links", ["node_a_id"])
op.create_index("ix_trust_links_node_b", "trust_links", ["node_b_id"])

# ── Add org_id to existing tables ────────────────────────────────────────
op.add_column("tasks", sa.Column("org_id", sa.String(64), nullable=True))
op.add_column("audit_events", sa.Column("org_id", sa.String(64), nullable=True))


def downgrade() -> None:
op.drop_column("audit_events", "org_id")
op.drop_column("tasks", "org_id")
op.drop_table("trust_links")
op.drop_table("org_invites")
op.drop_table("org_memberships")
op.drop_table("organizations")
Loading
Loading