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
15 changes: 15 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## Is this user-facing?

<!-- Required: choose one. See `runtime-e2e/README.md` for the convention. -->

- [ ] **Yes** — includes a runtime-path test that exercises this from the user's actual surface (OpenClaw tool / Claude skill / Cursor tool / Codex tool / portal page / SDK example invoked end-to-end through the live stack). Examples that import the SDK client class directly do NOT count.
- [ ] **No** — internal-only capability. Reason (must name a specific downstream consumer — a named test, a scheduled job, an internal CLI; "future PRs" or "wire later" is NOT acceptable): ___________

If user-facing, the wiring PR for every relevant runtime must land with this PR (or be linked and merged in the same release window). No "wire it later."

> **If a user cannot reach the feature from their runtime, we did not ship a feature, we shipped a library.**

See [`runtime-e2e/README.md`](../runtime-e2e/README.md) for the convention. The cross-plugin coverage matrix lives at `axonflow-internal-docs/engineering/FEATURE_RUNTIME_COVERAGE.md` (private; engineering team only).

---

## Description

<!-- Provide a clear and concise description of the changes in this PR -->
Expand Down
343 changes: 343 additions & 0 deletions .github/workflows/community-saas-daily-report.yml

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ community mirror, **Enterprise** changes are EE-only.

## [Unreleased]

## [7.6.1] - 2026-05-04 — Governance overrides + audit-search response fixes

PATCH release. Two user-visible bug fixes around the read-side governance
surface; no new endpoints, no schema-breaking changes on existing
responses. Companion to plugin releases axonflow-claude-plugin v1.1.0,
axonflow-cursor-plugin v1.1.0, axonflow-codex-plugin v1.1.0, and
axonflow-openclaw-plugin v2.1.0, which expose this surface as
agent-callable tools and skills.

> Note: the binary additionally contains internal scaffolding for
> upcoming work (free-tier email recovery, paid plugin-claim tier).
> These are not yet wired to any user-facing surface in this release —
> no new public endpoints, no behaviour change. They activate in a
> later release when the plugin and operator-facing pieces ship
> together.

**Bug fixes (Community):**

- **`POST /api/v1/audit/search` no longer returns `entries: null` on empty
result sets.** The response now consistently returns `entries: []` so
downstream clients that iterate the array (`for entry of entries`) or
read its length without a null guard work correctly. Pre-existing
callers that already handled the null case remain compatible.
- **`POST /api/v1/overrides` now rejects with HTTP 403 for severity=critical
system policies.** Authentication-bypass, time-based blind SQL
injection, stacked DROP/DELETE/UPDATE/INSERT/EXEC, government IDs,
and financial-PII patterns are no longer overridable; attempting to
create a session override against any of them returns
`403 "Critical-risk policies cannot be overridden"`. Pre-existing
active overrides on these policies are revoked at upgrade time.

## [7.6.0] - 2026-05-02 — Policy-engine response cleanup + per-category enforcement controls

MINOR release. Adds new API surfaces on the marketplace CFN template and
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.6.0
7.6.1
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ services:
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
AXONFLOW_INTEGRATIONS: ${AXONFLOW_INTEGRATIONS:-}
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-7.6.0}"
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-7.6.1}"

# Media governance (v4.5.0+) - set to "true" to enable in Community mode
MEDIA_GOVERNANCE_ENABLED: ${MEDIA_GOVERNANCE_ENABLED:-}
Expand Down Expand Up @@ -223,7 +223,7 @@ services:
PORT: 8081
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-7.6.0}"
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-7.6.1}"

# HITL mode (Evaluation+) — set to "true" to enable the HITL workflow
# engine backing WCP /steps/{step_id}/approve|reject, MAP plan-scoped
Expand Down
47 changes: 47 additions & 0 deletions migrations/core/075_community_saas_email_claim.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- Migration 075: Add email-claim columns to community_saas_registrations
-- Date: 2026-05-03
-- Context: Tenant durability + claim work (PRD: axonflow-business-docs/product/PRD_TENANT_DURABILITY_AND_CLAIM.md,
-- companion ADR-049-plugin-claimed-license-tier).
--
-- Provides:
-- community_saas_registrations.claimed_by_email — email address bound to a tenant for recovery.
-- Indexed but NOT unique. App-level cap of 3 active
-- tenants per email enforced at claim/recover time.
-- community_saas_registrations.claimed_at — timestamp when the binding was established.
-- idx_csaas_reg_claimed_email — partial index for email recovery lookups.
--
-- Why this exists:
-- The 2026-04-29 18:54Z cluster of 8 "registration not found" auth failures revealed that
-- tenant identity does not survive community-saas stack rotation: when a stack is replaced,
-- the new RDS instance has no row for tenant_ids that had been registered against the old
-- stack. Plugins holding cached credentials silently 401 the next time they make a call.
--
-- Email-bound recovery is the cross-stack continuity layer: a plugin that has set userEmail
-- in its config can present that email at /api/v1/recover, receive a magic link, and have a
-- fresh registration row issued under the same email — preserving the user's identity even
-- though the tenant_id itself rotates.
--
-- Why claimed_by_email is NOT unique:
-- Real users have multiple machines (laptop + work + personal). Forcing 1:1 email-to-tenant
-- would push power users to use throwaway emails or share tenant credentials across machines —
-- both worse than just allowing multiple tenants per email. App-level cap of 3 is enforced at
-- claim/recover time; cap is easy to raise; unique constraint would be hard to remove later.
--
-- Depends on: 068_community_saas_registrations
-- Companion: ADR-049-plugin-claimed-license-tier

ALTER TABLE community_saas_registrations
ADD COLUMN IF NOT EXISTS claimed_by_email VARCHAR(255),
ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMP WITH TIME ZONE;

-- Partial index — only emails that have been claimed.
-- Used by: /api/v1/recover endpoint (lookup all tenants for an email),
-- and by app-level cap check (count tenants per email at claim time).
CREATE INDEX IF NOT EXISTS idx_csaas_reg_claimed_email
ON community_saas_registrations(claimed_by_email)
WHERE claimed_by_email IS NOT NULL;

DO $$
BEGIN
RAISE NOTICE 'Migration 075: claimed_by_email + claimed_at columns added to community_saas_registrations';
END $$;
13 changes: 13 additions & 0 deletions migrations/core/075_community_saas_email_claim_down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Down migration for 075: drop email-claim columns + index from community_saas_registrations.
-- Idempotent.

DROP INDEX IF EXISTS idx_csaas_reg_claimed_email;

ALTER TABLE community_saas_registrations
DROP COLUMN IF EXISTS claimed_at,
DROP COLUMN IF EXISTS claimed_by_email;

DO $$
BEGIN
RAISE NOTICE 'Migration 075 down: dropped claimed_by_email + claimed_at + index';
END $$;
48 changes: 48 additions & 0 deletions migrations/core/076_community_saas_recovery_tokens.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- Migration 076: Community-SaaS recovery tokens for free email-recovery (W3)
-- Date: 2026-05-03
-- Context: Tenant durability + claim work (PRD: axonflow-internal-docs/prds/PRD_TENANT_DURABILITY_AND_CLAIM.md,
-- companion ADR-049-plugin-claimed-license-tier).
--
-- Provides:
-- community_saas_recovery_tokens — short-lived single-use magic-link tokens.
-- Issued by POST /api/v1/recover (email lookup),
-- consumed by GET /api/v1/recover/verify.
--
-- Why this exists:
-- Phase 0 confirmed the cross-stack continuity gap: when community-saas stacks
-- rotate, plugin caches hold credentials whose rows don't exist in the new RDS.
-- The W3 recovery flow lets users with email-bound tenants (claimed_by_email set
-- via either registration or POST /api/v1/claim) receive a magic link, click it,
-- and get a fresh tenant_id bound to the same email. Audit history before recovery
-- stays under the previous tenant_id (acceptable for free tier; Pro tier resolves
-- this differently via license-token-bound recovery in W4).
--
-- Token storage: token is HASHED before storage (SHA-256 — not bcrypt because
-- magic links are short-lived (15 min) and we need exact-match lookup, not
-- password-style verification). The plain token is sent in the magic-link URL
-- query parameter and never stored server-side after the row is written.
--
-- Depends on: 075_community_saas_email_claim (claimed_by_email column on registrations)

CREATE TABLE IF NOT EXISTS community_saas_recovery_tokens (
token_hash VARCHAR(64) PRIMARY KEY, -- SHA-256 hex of the magic-link token
email VARCHAR(255) NOT NULL, -- target email for the recovery
requesting_ip_hash VARCHAR(64), -- SHA-256 hex of the IP that requested (for audit)
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL, -- typically NOW() + 15 minutes
consumed_at TIMESTAMP WITH TIME ZONE, -- set when verify endpoint successfully exchanges the token
consumed_by_tenant VARCHAR(255) -- the new tenant_id issued on successful exchange (audit trail)
);

-- Index for cleanup queries (purge expired/consumed tokens older than 7 days)
CREATE INDEX IF NOT EXISTS idx_csaas_recovery_expires
ON community_saas_recovery_tokens(expires_at);

-- Index for per-email rate limit lookups (block if too many recent tokens for same email)
CREATE INDEX IF NOT EXISTS idx_csaas_recovery_email_recent
ON community_saas_recovery_tokens(email, created_at DESC);

DO $$
BEGIN
RAISE NOTICE 'Migration 076: community_saas_recovery_tokens table created';
END $$;
11 changes: 11 additions & 0 deletions migrations/core/076_community_saas_recovery_tokens_down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Down migration for 076: drop recovery_tokens table.
-- Idempotent.

DROP INDEX IF EXISTS idx_csaas_recovery_email_recent;
DROP INDEX IF EXISTS idx_csaas_recovery_expires;
DROP TABLE IF EXISTS community_saas_recovery_tokens;

DO $$
BEGIN
RAISE NOTICE 'Migration 076 down: dropped community_saas_recovery_tokens table';
END $$;
52 changes: 52 additions & 0 deletions migrations/core/076_critical_system_policies_no_override.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-- Migration 076: Tighten allow_override on severity='critical' system policies.
--
-- Migration 070 added risk_level + allow_override columns and seeded existing
-- rows via category-based mapping. That mapping only flipped allow_override
-- to FALSE for categories `dangerous-command`, `rce`, `privilege-escalation`,
-- `system-destruction` — none of which exist in the seeded system policy set
-- (migration 031). The result: zero system policies had allow_override=FALSE
-- in production, leaving the createOverrideHandler 403 enforcement path
-- (platform/orchestrator/overrides_handler.go:343) unreachable for any user.
--
-- Severity='critical' system policies — auth bypass, time-based blind injection,
-- stacked DROP/DELETE/UPDATE/INSERT/EXEC, government IDs, financial PII —
-- are precisely the cases where session override should be denied at create
-- time. Promoting them to risk_level='critical' also engages the migration 070
-- DB trigger (`enforce_critical_no_override`), which reaffirms allow_override
-- can never be flipped back to TRUE on these rows.
--
-- Scope: only tier='system' rows whose risk_level isn't already 'critical' so
-- the migration is idempotent under repeat application.

BEGIN;

UPDATE static_policies
SET risk_level = 'critical',
allow_override = FALSE
WHERE tier = 'system'
AND severity = 'critical'
AND risk_level <> 'critical';

-- Revoke any existing active overrides on policies that just became
-- non-overridable. Per ADR-044: "when a policy's allow_override flips to
-- false, all active overrides for that policy are revoked with reason
-- policy_changed". Note that the ADR also specifies an `override_revoked`
-- audit event; SQL migrations cannot reach the application-level audit
-- logger, so the revocation is recorded in revoked_at/revoked_by on the
-- row itself (queryable via /api/v1/overrides?include_revoked=true).
-- Selection criteria mirror the UPDATE above directly rather than reading
-- the just-flipped allow_override column, to keep the intent
-- transaction-order-independent.
UPDATE policy_overrides po
SET revoked_at = NOW(),
revoked_by = 'system:migration-076',
updated_at = NOW(),
updated_by = 'system:migration-076'
FROM static_policies sp
WHERE po.policy_id::text = sp.id::text
AND po.policy_type = 'static'
AND po.revoked_at IS NULL
AND sp.tier = 'system'
AND sp.severity = 'critical';

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Down migration for 076.
--
-- Restores the category-based mapping from migration 070 for severity='critical'
-- system policies that this migration promoted to risk_level='critical'.
-- security-sqli reverts to 'high'; PII categories revert to 'medium' (the
-- migration-070 default).
--
-- Note: the migration-070 trigger `enforce_critical_no_override` forced
-- allow_override=FALSE while risk_level was 'critical'. Once we drop back to
-- 'high' or 'medium', allow_override is set to TRUE explicitly so the row
-- matches the post-migration-070 baseline.
--
-- Cascaded `policy_overrides` revocations from the forward migration are
-- intentionally NOT reversed — revocation is auditable history and we don't
-- rewrite it. Operators who need an active override after rolling back
-- should re-create it through the regular POST /api/v1/overrides path.

BEGIN;

UPDATE static_policies
SET risk_level = CASE
WHEN category IN ('security-sqli', 'prompt-injection', 'secret-leak') THEN 'high'
ELSE 'medium'
END,
allow_override = TRUE
WHERE tier = 'system'
AND severity = 'critical'
AND risk_level = 'critical';

COMMIT;
79 changes: 79 additions & 0 deletions migrations/core/077_plugin_user_licenses.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
-- Migration 077: Plugin user licenses for paid Pro tier (W4)
-- Date: 2026-05-04
-- Context: Tenant durability + claim work (PRD: axonflow-internal-docs/prds/PRD_TENANT_DURABILITY_AND_CLAIM.md,
-- ADR: axonflow-enterprise/technical-docs/architecture-decisions/ADR-049-plugin-claimed-license-tier.md).
--
-- Provides:
-- plugin_user_licenses — DB-resident entitlements for paid plugin-claimed
-- and (future) plugin-subscription tiers. Source of
-- truth for ENFORCEMENT (retention, quota, capabilities,
-- support level). Token (sent by plugin in
-- X-License-Token header) carries identity + coarse
-- tier; this table carries the mutable entitlements.
--
-- Why hybrid schema (hot indexed columns + JSONB):
-- Per ADR-049 sections 4 + 9, the agent middleware queries this row on
-- every request. Hot fields (tier, expires_at, revoked_at, license_token_jti)
-- are top-level indexed columns for fast enforcement queries. Everything else
-- lives in JSONB so we can add capabilities (e.g. for Premium v2) without
-- schema migrations.
--
-- Why per-request validation instead of session caching (ADR-049 section 2):
-- - Plugin-claim revocation must be effective within ~60s (chargeback / dispute)
-- - Per-tenant DB row is already cached in the agent's existing tenant lookup
-- - Avoids stale-token-after-revoke window that session caching would introduce
--
-- Depends on: 075_community_saas_email_claim (claimed_by_email column on
-- community_saas_registrations — referenced via FK from this table)

CREATE TABLE IF NOT EXISTS plugin_user_licenses (
license_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

-- Identity binding (FK to the registrations table — same DB per ADR-049 section 6)
tenant_id VARCHAR(255) NOT NULL REFERENCES community_saas_registrations(tenant_id),
claimed_by_email VARCHAR(255) NOT NULL,

-- Hot indexed columns (enforcement path: agent middleware reads these on every request)
tier VARCHAR(50) NOT NULL CHECK (tier IN ('plugin-claimed', 'plugin-subscription')),
expires_at TIMESTAMP WITH TIME ZONE, -- NULL for one-time purchases (Pro v1); future timestamp for future subscription tier
revoked_at TIMESTAMP WITH TIME ZONE,
license_token_jti VARCHAR(64) NOT NULL UNIQUE, -- JWT-style jti claim; enables per-token revocation + audit trail
rotation_generation INTEGER NOT NULL DEFAULT 1, -- which signing-key generation issued this token

-- Mutable entitlements as JSONB so we can add capabilities without migrations
-- For tier=plugin-claimed (Pro v1):
-- { "retention_days": 365, "daily_event_quota": 10000, "email_recovery": true,
-- "license_token_recovery": true, "read_tools": true, "write_hooks": true,
-- "advanced_hosted_capabilities": [], "support_level": "best_effort_email" }
-- For tier=plugin-subscription (Premium v2 placeholder; not issued in v1):
-- { ..., "daily_event_quota": 50000, "support_level": "priority_email_no_sla",
-- "advanced_hosted_capabilities": ["map_plans", ...] }
entitlements JSONB NOT NULL DEFAULT '{}'::jsonb,

-- Audit + payment trail (for refund / dispute / accounting reconciliation)
stripe_customer_id VARCHAR(255),
stripe_session_id VARCHAR(255),
issued_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
revocation_reason TEXT
);

-- Hot indexes for the enforcement path. agent middleware queries by tenant_id
-- on every request, so this needs to be fast.
CREATE INDEX IF NOT EXISTS idx_plugin_lic_tenant ON plugin_user_licenses(tenant_id);

-- Active-only partial index — most queries filter to non-revoked rows.
-- Partial index is much smaller than a full index, faster lookups.
CREATE INDEX IF NOT EXISTS idx_plugin_lic_active ON plugin_user_licenses(tenant_id) WHERE revoked_at IS NULL;

-- Email lookups for the recovery flow + per-email-tenant-cap queries.
CREATE INDEX IF NOT EXISTS idx_plugin_lic_email ON plugin_user_licenses(claimed_by_email);

-- jti lookups for token revocation + audit trail correlation.
-- license_token_jti is already UNIQUE-constrained (creates an implicit index),
-- but explicit naming makes the index discoverable in pg_indexes for ops.
CREATE INDEX IF NOT EXISTS idx_plugin_lic_jti ON plugin_user_licenses(license_token_jti);

DO $$
BEGIN
RAISE NOTICE 'Migration 077: plugin_user_licenses table created (W4 paid Pro tier infrastructure)';
END $$;
14 changes: 14 additions & 0 deletions migrations/core/077_plugin_user_licenses_down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Down migration for 077: drop plugin_user_licenses table.
-- Idempotent.

DROP INDEX IF EXISTS idx_plugin_lic_jti;
DROP INDEX IF EXISTS idx_plugin_lic_email;
DROP INDEX IF EXISTS idx_plugin_lic_active;
DROP INDEX IF EXISTS idx_plugin_lic_tenant;

DROP TABLE IF EXISTS plugin_user_licenses;

DO $$
BEGIN
RAISE NOTICE 'Migration 077 down: dropped plugin_user_licenses table';
END $$;
Loading
Loading