kyte-php v4.4.0
Phase 2 (MCP server) + Phase 2.5 (sensitive-data flag) + Phase 3 (JWT auth) all ship together. No breaking changes — every addition is opt-in, defaults preserve v4.3.x behavior bit-for-bit. Existing customers can upgrade in place without code changes.
New Feature: Embedded MCP server (Phase 2)
Each Kyte install can now expose a Model Context Protocol endpoint that lets AI clients (Claude Code, Claude.ai) inspect controllers, models, pages, and functions over a tenant-scoped bearer.
- New endpoint:
POST /mcp— handled byMcp\Endpointoutside the standard MVC pipeline. JSON-RPC over HTTP, protocol version2025-06-18. Bypasses Kyte's HMAC envelope. - New strategy:
McpTokenStrategyregistered with the auth dispatcher. StrictAuthorization: Bearer kmcp_live_…prefix match. - New model:
KyteMCPToken— opaque bearer tokens. Stored as sha256 hash, displayed as 16-char prefix for identification. Scoped (read/draft/commit), revokable, optional expiry, optional CIDR allowlist, optional application-binding. - New controller:
KyteMCPTokenController— issue, list, revoke. Generates the raw token at issuance (returned once, never recoverable). Force-overrideskyte_accountfrom auth context to close a privilege-escalation vector. EmitsMCP_TOKEN_ISSUE/MCP_TOKEN_REVOKE/MCP_TOKEN_USE/MCP_SCOPE_VIOLATIONaudit rows. - 10 read tools shipped:
list_applications,list_controllers,read_controller,list_functions,read_function(with optionalversion_number),list_models,read_model,list_sites,list_pages,read_page(with optionalversion_number). - Scope enforcement:
ScopedCallToolHandlerregistered ahead of the SDK's default handler. Tools declare required scope via#[RequiresScope('read'|'draft'|'commit')]attribute. Fail-closed default — a tool without the attribute is unreachable. - Account isolation: every tool re-asserts
entity.kyte_account === api.account.id. Foreign ids returnnull/[], never the foreign record. - bzip2 decompression:
Controller.code,Function.code, andKytePageVersionContent.{html,stylesheet,javascript}are stored compressed;Bz2Codec::decompressIfBz2wraps the read tools so source is returned in plaintext to the client. - Origin validation on
/mcp: per spec § Security, mitigates DNS rebinding. Policy is "no Origin → allow, Origin present → checkMCP_ALLOWED_ORIGINSconstant (CSV)". CLI clients (Claude Code) unaffected; restrictive default forces operator opt-in for browser origins. - Proxy-aware client IP:
Kyte\Mcp\Util\ClientIpreadsCF-Connecting-IPthenX-Forwarded-Forfirst hop then falls back toREMOTE_ADDR. Gated onKYTE_TRUST_PROXY_IP_HEADERSconstant — default-off so installs without a proxy aren't exposed to header spoofing. Wired intoMcpTokenStrategy::clientIp()for IP allowlist enforcement and audit fields.
New Feature: Sensitive-data flag (Phase 2.5)
A three-tier opt-in flag that prevents activity/error logs from capturing request bodies, and gates MCP read tools from exposing source for flagged entities. Designed for pass-through controllers whose payload contents are regulated data the platform should not store.
- New columns (default
0, no behavior change unless flipped):Controller.sensitive— blanket flag. When1, drops body+response from logs entirely. Handles virtual (no-model) pass-through controllers.DataModel.sensitive— same blanket treatment when the model is the request target.ModelAttribute.sensitive— per-field redaction. Distinct from existing.protected(which only blanks values in GET responses). Set both for both behaviors.KytePage.sensitive— MCP-only; withholdshtml/stylesheet/javascriptfromread_page. Pages don't write to activity logs.
- New service:
SensitivityPolicy(src/Core/SensitivityPolicy.php) — single source of truth, per-request singleton with in-memory cache keyed by(scope, name, account). One DB hit per tuple per request. Fail-permissive on lookup error so transient DB issues degrade to existingSENSITIVE_FIELDSbaseline rather than to no redaction at all. - ActivityLogger consults the policy before persisting
request_dataand the PUT changes diff. Blanket-sensitive → both fields null. Field-sensitive → flagged fields replaced with[REDACTED], other fields pass through. The hardcodedSENSITIVE_FIELDSlist (password / token / secret_key / etc.) still runs as a baseline on top. - ErrorHandler previously captured request body AND response payload into
KyteErrorwith zero redaction — closed in this release.handleException,handleError, andoutputBufferCallbackall consult the policy now. AI error-correction queue (AIErrorCorrection::queueForAnalysis) gains its own defense-in-depth check at the top: skips any sensitive-origin row, with audit-log breadcrumb. Regulated data never reaches Anthropic for analysis. - MCP read tools gated by the same flag: sensitive controller →
read_controllerreturnscode: null+sensitive: true; sensitive function (via parent controller) → same; sensitive model →read_modelreturnsdefinition: null+sensitive: true; sensitive field → stripped fromdefinition.struct, listed separately insensitive_fields; sensitive page → content fields null. List tools (list_controllers,list_models,list_pages) surface asensitive: boolon each row so AI clients see up front which entities are gated. - Runtime API responses unaffected — a sensitive controller still returns its normal response to the caller. The flag governs log / MCP / AI exposure only, not the live contract.
New Feature: JWT bearer authentication (Phase 3)
Modern auth path that coexists with the legacy HMAC sign/rotate. Customers can run a mix of HMAC apps and JWT apps on the same install.
- New strategy:
JwtSessionStrategy— HS256 access tokens with a configurable secret, claims{iss, sub, aud, exp, iat, nbf, jti, email, app}. StrictAuthorization: Bearer eyJ…prefix match (won't clash with MCP'skmcp_live_…). Decode + verify inpreAuth, sets$api->user,$api->account, and$api->session->hasSession = trueso the standard ModelController auth gate accepts the request. - New endpoint family:
POST /jwt/login,POST /jwt/refresh,POST /jwt/logout,POST /jwt/logout-all— handled byJwtEndpoint(same pattern asMcp\Endpoint, bypasses the MVC pipeline so login can run pre-auth). Login posts{email, password, app_identifier?}and returns{access_token, refresh_token, expires_in, token_type:'Bearer', refresh_expires_at}. - Refresh tokens: opaque (
kref_v1_…prefix), stored as sha256 hash in newKyteRefreshTokentable. Single-use rotation per RFC 6819 — every successful refresh revokes the presented token and issues a new one in the same family. Reuse detection: presenting a revoked token revokes the entire family (likely leak signal). Expiration without revocation does not trigger family kill. - Multilogon: separate logins always create separate families. A user signing in from laptop and phone gets two independent families — revoking one device does not affect the other. Distinct from HMAC's
ALLOW_MULTILOGONflag. AuthDispatcher::buildDefault()now registers three strategies: McpToken → JwtSession → Hmac. Eachmatches()is strict so order doesn't affect correctness; order is documented for review clarity. HMAC clients continue working unchanged viaHmacSessionStrategy.Application.auth_modecolumn added:'hmac'(default) or'jwt'. Drives whether Shipyard's page generator emits the v1.x HMAC constructor or the v2 JWT constructor for kyte-api-js consumers.- Configuration constants (define in
config.phpto enable JWT):KYTE_JWT_SECRET— required for JWT mode. At least 256 bits of entropy. Never commit to version control.KYTE_JWT_ISSUER— optional, defaults to'kyte'. Mismatched tokens are rejected at preAuth.KYTE_JWT_ACCESS_TTL— optional, default 900 seconds.KYTE_JWT_REFRESH_TTL— optional, default 604800 seconds (7 days).
- Dependency:
firebase/php-jwt ^7.0— lightweight, well-known, no transitive deps.
Bundled migrations
Three new SQL files in migrations/. Apply in order. All are additive (new columns / new table) — safe to apply ahead of code; new columns default to 0 / 'hmac' so legacy code sees no behavior change until the matching feature is opted into.
migrations/4.4.0_sensitive_columns.sql # Controller / DataModel / ModelAttribute / KytePage .sensitive
migrations/4.4.0_jwt_refresh_tokens.sql # KyteRefreshToken table
migrations/4.4.0_application_auth_mode.sql # Application.auth_mode
CI hardening
- PHP matrix in
.github/workflows/php.yml: tests now run on PHP 8.2 AND 8.3 against MariaDB 10.5.29. Catches version-specific syntax / deprecation issues. - PHPStan static analysis at level 1 with a baseline of pre-existing findings (
phpstan-baseline.neon). New code is held to zero level-1 violations; baseline only shrinks. - Composer audit step on every push — fails CI on a new advisory in production dependencies.
Test coverage
- 180 unit tests, 531 assertions. Up from 109 at end of Phase 2.
- New test files:
SensitivityPolicyTest,ActivityLoggerSensitivityTest,ErrorHandlerSensitivityTest,McpSensitivityTest,JwtSessionStrategyTest,JwtEndpointTest,RefreshTokenStoreTest. - Includes a regression test for the
hasSessionintegration bug surfaced during dev rollout: JwtSessionStrategy must mark$api->session->hasSession = trueafter validating the bearer, otherwiseModelController::authenticate()rejects every protected endpoint despite a valid JWT.
Upgrade notes
- Apply the three migrations above to your database.
- If using JWT mode: define
KYTE_JWT_SECRETinconfig.php(generate withopenssl rand -base64 48). HMAC-only installs need no config change. - If you were running with
AUTH_STRATEGY_DISPATCHER='shadow'from v4.3.0, you can now safely flip to'on'to activate the dispatcher. HMAC traffic routes through the newHmacSessionStrategywhich has been shadow-verified bit-identical to the inline auth. - kyte-api-js v2.0+ is required on the client side for JWT apps. v1.x continues to work unchanged for HMAC apps.