diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2b25c16 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.git +.github +.tmp* +dist +coverage +*.log diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..54d3fbb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..778d02d --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,25 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '30 3 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 diff --git a/Dockerfile.enterprise b/Dockerfile.enterprise new file mode 100644 index 0000000..8e88a4a --- /dev/null +++ b/Dockerfile.enterprise @@ -0,0 +1,21 @@ +FROM node:22-bookworm-slim AS build +WORKDIR /app +COPY package*.json ./ +COPY packages/core/package*.json packages/core/package.json ./packages/core/ +COPY packages/enterprise/package*.json packages/enterprise/package.json ./packages/enterprise/ +RUN npm install +COPY . . +RUN npm run build:core && npm run build:enterprise + +FROM node:22-bookworm-slim AS runtime +ENV NODE_ENV=production +WORKDIR /app +RUN useradd --system --create-home --uid 10001 authority +COPY --from=build /app/package*.json ./ +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/packages/core/dist ./packages/core/dist +COPY --from=build /app/packages/enterprise/dist ./packages/enterprise/dist +COPY --from=build /app/packages/enterprise/package.json ./packages/enterprise/package.json +USER authority +EXPOSE 8080 +CMD ["node", "packages/enterprise/dist/serve.js"] diff --git a/docs/PRODUCTION-DEPLOYMENT.md b/docs/PRODUCTION-DEPLOYMENT.md new file mode 100644 index 0000000..64aec3d --- /dev/null +++ b/docs/PRODUCTION-DEPLOYMENT.md @@ -0,0 +1,47 @@ +# Enterprise production deployment + +The enterprise package has two runtime modes: + +- `InMemoryEnterpriseStore` for tests and local development. +- `PostgresEnterpriseStore` for production deployments. + +The production entrypoint requires PostgreSQL and OIDC/JWKS configuration. + +## Required environment + +Copy `packages/enterprise/.env.example` and provide real values through your deployment secret/configuration system. Do not commit credentials. + +Required: + +- `DATABASE_URL` +- `AUTHORITY_JWT_ISSUER` +- `AUTHORITY_JWT_AUDIENCE` +- `AUTHORITY_JWKS_URI` + +The service listens on port `8080` by default. + +## Database + +Apply `packages/enterprise/migrations/001_enterprise.sql` through the organization's migration process. The runtime also performs an idempotent schema initialization so a reference deployment can bootstrap itself, but mature production environments should treat migrations as the authoritative schema lifecycle. + +Use a PostgreSQL deployment with encryption in transit, encryption at rest, automated backups, point-in-time recovery, monitoring, and a documented recovery procedure. + +## Network + +Terminate TLS at the managed load balancer or ingress layer. Restrict network access so the authorization service and database are reachable only from trusted application paths. Put rate limiting and request-size limits at the edge as well as the service. + +## Identity + +Use short-lived OIDC access tokens. Validate issuer, audience, signature, expiration, not-before, algorithm, and signing key identifier. Tenant identity comes from the authenticated principal rather than a client-selected routing value. + +## Authorization + +Keep policy administration, contract administration, security administration, and approval authority separated organizationally where practical. A human approval must bind to the exact action digest and must not be issued by the requester. + +## Operations + +Export authorization events and receipts to the organization's audit/SIEM pipeline. Monitor denied actions, repeated nonce failures, approval failures, authentication failures, and database health. Protect signing keys with KMS/HSM-backed systems when deployment architecture requires server-managed signing keys. + +## Security assessment + +Before allowing high-impact financial, production, destructive, or privileged actions, run an independent application and infrastructure security assessment against the deployed service. The repository's red-team tests are regression tests, not a substitute for an external assessment. diff --git a/packages/enterprise/.env.example b/packages/enterprise/.env.example new file mode 100644 index 0000000..fc71052 --- /dev/null +++ b/packages/enterprise/.env.example @@ -0,0 +1,15 @@ +# Runtime +NODE_ENV=production +HOST=0.0.0.0 +PORT=8080 +DB_POOL_MAX=20 + +# PostgreSQL +DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/agent_authority +DATABASE_SSL=true +DATABASE_SSL_REJECT_UNAUTHORIZED=true + +# OIDC/JWKS +AUTHORITY_JWT_ISSUER=https://idp.example.com/ +AUTHORITY_JWT_AUDIENCE=agent-authority +AUTHORITY_JWKS_URI=https://idp.example.com/.well-known/jwks.json diff --git a/packages/enterprise/migrations/001_enterprise.sql b/packages/enterprise/migrations/001_enterprise.sql new file mode 100644 index 0000000..d927214 --- /dev/null +++ b/packages/enterprise/migrations/001_enterprise.sql @@ -0,0 +1,61 @@ +CREATE TABLE IF NOT EXISTS aa_enterprise_policies ( + tenant_id TEXT NOT NULL, + policy_id TEXT NOT NULL, + version INTEGER NOT NULL, + status TEXT NOT NULL CHECK (status IN ('draft','active','retired')), + document JSONB NOT NULL, + created_by TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + activated_at TIMESTAMPTZ, + PRIMARY KEY (tenant_id, policy_id, version) +); + +CREATE UNIQUE INDEX IF NOT EXISTS aa_enterprise_active_policy + ON aa_enterprise_policies (tenant_id, policy_id) WHERE status='active'; + +CREATE TABLE IF NOT EXISTS aa_enterprise_contracts ( + tenant_id TEXT NOT NULL, + contract_id TEXT NOT NULL, + document JSONB NOT NULL, + revoked_at TIMESTAMPTZ, + PRIMARY KEY (tenant_id, contract_id) +); + +CREATE TABLE IF NOT EXISTS aa_enterprise_nonces ( + tenant_id TEXT NOT NULL, + nonce TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (tenant_id, nonce) +); + +CREATE TABLE IF NOT EXISTS aa_enterprise_events ( + tenant_id TEXT NOT NULL, + event_id TEXT NOT NULL, + correlation_id TEXT NOT NULL, + document JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (tenant_id, event_id) +); + +CREATE TABLE IF NOT EXISTS aa_enterprise_approvals ( + tenant_id TEXT NOT NULL, + approval_id TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending','approved','rejected','consumed')), + request JSONB NOT NULL, + receipt JSONB, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (tenant_id, approval_id) +); + +CREATE TABLE IF NOT EXISTS aa_enterprise_receipts ( + tenant_id TEXT NOT NULL, + receipt_id TEXT NOT NULL, + event_id TEXT NOT NULL, + action_digest TEXT NOT NULL, + previous_receipt_hash TEXT, + receipt_hash TEXT NOT NULL, + document JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (tenant_id, receipt_id) +);