Skip to content

Releases: keyqcloud/kyte-php

kyte-php v4.15.1

Choose a tag to compare

@github-actions github-actions released this 01 Jul 09:00
97e5a08

Fix: make the kyte_locked migration portable to MySQL — KYTE-#325

  • migrations/4.15.0_datamodel_kyte_locked.sql used MariaDB-only ALTER TABLE ... ADD COLUMN IF NOT EXISTS, which is a syntax error on MySQL 5.7 / 8.0. Rewritten to guard with an information_schema column check + a prepared statement, which is idempotent on both MySQL and MariaDB. Surfaced during the v4.15.0 rollout to the first MySQL-backed install (its kyte_locked columns already existed, so nothing was harmed — the failed statement was a no-op — but a drifted MySQL install would have errored and never gained the column, leaving the model-level lock guard inert).
  • Migration-only change; no PHP behavior change. Installs already on v4.15.0 with the columns present need no action. Fresh or drifted MySQL installs now apply cleanly.

kyte-php v4.15.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 14:32
daa169f

Feature: MCP schema-management tools (create/alter/drop models) + schema scope — KYTE-#325

A schema-scoped MCP token can now create and migrate data models from Claude, where each change applies a real DDL migration (CREATE/ALTER/DROP TABLE/COLUMN) against the application's own database. Same plumbing as the #322 site tools — the tools drive DataModelController/ModelAttributeController in internal mode. Shipped in two parts.

PR B — MCP schema tools:

  • New schema scope added to KyteMCPTokenController::VALID_SCOPES, held separately from provision so a token can be granted model-migration rights independently of site spin-up/tear-down. Not hierarchical.
  • src/Mcp/Tools/ModelTools.php (auto-discovered) write tools, all [schema]: create_model (CREATE TABLE), add_attribute (ADD COLUMN — decimal needs precision+scale, varchar needs size, FK via foreign_key_model), update_attribute (CHANGE COLUMN — name required since the column def is rewritten in full), rename_model (RENAME TABLE); plus the destructive remove_attribute (DROP COLUMN) and delete_model (DROP TABLE), each gated behind a required confirm_destructive flag. read_model/list_models already existed. Every caller-supplied model/attribute id is re-scoped to the token's account; foreign ids are rejected.
  • rename_model registers the app's model constants via Api::loadAppModels first — DataModelController::update guards on defined($o->name), which the HTTP pipeline satisfies at routing but the MCP path (routing-bypassed) does not (mirrors AppModelWrapperController).
  • Model-layer hardening surfaced by the partial payloads the MCP tools send: DataModelController::prepareModelDef reads optional flags/defaults defensively (no undefined-property warning / null-to-strlen deprecation when protected/password/defaults/unsigned are omitted); Api::loadAppModels skips rows with a missing/corrupt model_definition instead of define()-ing a null name, and no longer re-defines an already-defined constant.
  • Tests: tests/McpModelToolsTest.php extended with write coverage (create/add/update/rename, decimal precision/scale reaching the physical column, destructive-op confirm gating, FK-dependency block, cross-account isolation), driving real DDL against the test DB via a dedicated password-bearing app user.

PR A — model-layer fixes/hardening (platform; benefits Shipyard too):

  • DBI::renameTable referenced an undefined $tbl_name_news (typo) → emitted a malformed RENAME; rename was effectively broken. Fixed to $tbl_name_new.
  • DBI::dropTable now uses DROP TABLE IF EXISTS (was a bare DROP that errored on an already-absent table).
  • kyte_locked enforced on the DDL pathDataModelController/ModelAttributeController now refuse update/delete of a locked model or attribute, guarding before any DB switch or DDL. New migrations/4.15.0_datamodel_kyte_locked.sql (idempotent ADD COLUMN IF NOT EXISTS) guarantees the column exists on both DataModel and ModelAttribute — the original consolidated 3.x→4.8 upgrade added it but some installs applied it only partially (observed: ModelAttribute had it, DataModel did not), leaving the model-level guard silently inert. The guard is only meaningful when the column physically exists.
  • FK-dependency guard before drop — dropping a model is refused while another model's attribute references it via foreignKeyModel; dropping a column is refused while another attribute references it via foreignKeyAttribute. Both account-scoped, with an error naming the blocking model.attribute. Replaces the two stale "external tables and foreign keys" TODOs.
  • Decimal (d) support — new migrations/4.15.0_modelattribute_decimal.sql adds precision/scale to ModelAttribute; prepareModelDef passes them through for type d; DBI::buildFieldDefinition now throws on a d column missing precision/scale instead of emitting invalid SQL. The type was previously non-functional end-to-end.
  • Model-cache invalidation — both controllers call Api::clearModelCache() after a schema change (create/rename/delete model, add/change/drop column), so the new struct is served immediately instead of the stale file cache (up to 1h TTL).

Migrations: migrations/4.15.0_modelattribute_decimal.sql (adds precision/scale) and migrations/4.15.0_datamodel_kyte_locked.sql (ensures kyte_locked on DataModel + ModelAttribute). Both additive, nullable, idempotent, no-op for existing rows. Roll with the batched 4.14.0 site-tools rollout.

kyte-php v4.14.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 09:19
ac437d9

Feature: MCP site-provisioning tools (create/delete/update/read) + provision scope — KYTE-#322

New MCP tools let a token holder manage a site's lifecycle from Claude, building on the #201 SiteProvisioningWorker (provisioning now runs in-process, so these tools just flip state and the worker does the AWS work out of band).

  • New provision scope — gates infrastructure-creating tools, held separately from content read/draft/commit so a token can author pages/scripts without the right to spin up or tear down sites. The dispatch path has no scope enum (RequiresScope/ScopeRegistry/the token CSV all accept arbitrary strings), so the only change to make it grantable was adding provision to KyteMCPTokenController::VALID_SCOPES.
  • src/Mcp/Tools/SiteTools.php (auto-discovered): create_site / delete_site / update_site [provision], read_site [read]. Mutations call KyteSiteController in internal mode (no session, mirroring DraftService's publish path); create_site/delete_site flip KyteSite.status to creating/deleting and return immediately — callers poll read_site (which surfaces status, provisioning_message, and the live cloudfront_domain) until active/deleted. Every caller-supplied id is re-scoped to the token's account; new sites are stamped with the caller account.
  • Guarded KyteSiteController::delete()'s deleted_by = $this->user->id for the no-KyteUser server-side path (isset() ? : 0).
  • Custom-domain (aliasDomain) assignment is intentionally not exposed here — that's the ACM + DNS path (KYTE-#320). Infra ids (bucket/distribution) omitted from output; cloudfront_domain surfaced.
  • Tests: tests/McpSiteToolsTest.php (row-level create/read/update/delete + account isolation), wired into the CI unit suite. Code-only — no migration.

kyte-php v4.13.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 01:57
d5ae201

Migration: site provisioning moved from Lambda into a cron worker — KYTE-#201

KyteSiteController published S3/CloudFront/ACM actions to SNS_QUEUE_SITE_MANAGEMENT → the kyte-lambda-site-management self-chaining state machine did the work, writing back to the DB via the kyte-lambda-database-transaction Lambda. Both Lambdas are replaced by a cron worker that writes the DB directly. Completes the #201 migration off SNS/Lambda (after #1 + #2 in 4.12.0).

  • SiteProvisioningWorker (src/Cron/SiteProvisioningWorker.php, CronJobBase, interval 30s) scans KyteSite rows in creating/deleting and advances each one actionable step per tick. Sub-state is inferred from the populated s3*/cf* columns (no separate state machine), and every AWS op is idempotent so a tick is safe to re-run:
    • CREATE: website bucket (create [us-east-1-aware] → drop public-access-block → public-read policy → website config) → media bucket (CORS instead) → website CloudFront → media CloudFront (each distribution id persisted the instant it's created, since createDistribution isn't idempotent) → poll both until Deployedstatus=active.
    • DELETE: empty+delete each bucket → disable+delete each distribution (polled across ticks) → delete ACM certs + Domain/SAN rows (after the distributions are gone — a cert can't be deleted while attached) → status=deleted.
    • CloudFront create/delete take minutes, so deployment is polled across ticks with heartbeat() — never a blocking sleep. A site that errors MAX_ATTEMPTS times flips to status=failed with provisioning_message instead of looping.
  • KyteSiteController new/delete no longer publish to SNS — they just set status=creating/deleting (+ the existing content-record cleanup) and let the worker take over. Domain/cert teardown moved to the worker (ordering depends on the distribution being deleted first).
  • Kyte\Aws wrapper fixes: S3::createBucket no longer sends a LocationConstraint for us-east-1 (it was failing there) and is idempotent on BucketAlreadyOwnedByYou; new S3::emptyBucket() (paginated, version-aware) so deleteBucket() works; CloudFront now applies DefaultTTL (86400) and the worker sets MinTTL=3600 for parity with the old Lambda; new CloudFront::isEnabled().
  • New: migrations/4.13.0_site_provisioning.sql (adds KyteSite.provisioning_message + provisioning_attempts), bin/register-site-provisioning-job.php.
  • Unlocks native MCP create_site/create_app. After this soaks, all 3 Lambdas + SNS_QUEUE_SITE_MANAGEMENT can be decommissioned.

kyte-php v4.12.0

Choose a tag to compare

@github-actions github-actions released this 29 Jun 12:09
5622c15

Cleanup: remove KYTE_USE_SNS flag + dead SNS invalidation branches — KYTE-#201

Direct CloudFront invalidation (shipped + verified in 4.11.1) is now the only path. With every install already running KYTE_USE_SNS=false, this removes the flag and the now-dead SNS publish branch at every invalidation site — behavior-neutral cleanup.

  • Dropped the if (KYTE_USE_SNS) { <SNS publish> } else { <direct> } fork at all 10 sites, keeping only the direct Kyte\Aws\CloudFront::createInvalidation() call inside its existing best-effort try/catch: ApplicationController (republish-all), KytePageController ×3, KyteScriptController ×2 (invalidateCloudFront/invalidateCloudFrontForDeletion), KyteLibraryController ×2, KytePageDataController, NavigationController, SideNavController.
  • Removed 'KYTE_USE_SNS' => false from Api::$defaultEnvironmentConstants and the define('KYTE_USE_SNS', ...) from sample-config.php + docs.
  • Kept SNS_REGION, SNS_QUEUE_SITE_MANAGEMENT and the Kyte\Aws\Sns class — still used by site-provisioning (migrated in later #201 work). Updated the PHPStan baseline accordingly. (SNS_KYTE_SHIPYARD_UPDATE is no longer referenced after the shipyard-update migration below.)

Fix: per-app DB connections (DBI::connectApp()) now use SSL when KYTE_DB_CA_BUNDLE is set

DBI::connectApp() — the per-application/tenant connection used by Api::dbappconnect() — opened a plain mysqli connection with no TLS, unlike DBI::connect(). Against a database with require_secure_transport=ON this fails with "Connections using insecure transport are prohibited": the control-plane connection (already SSL) succeeds so login works, but opening any application backed by a dedicated per-app database fails. Latent until a deployment runs on an SSL-required server — surfaced migrating the dev server from Aurora (require_secure_transport=OFF) to a MariaDB RDS with it ON.

  • connectApp() now mirrors connect(): when KYTE_DB_CA_BUNDLE is defined it connects via ssl_set() + MYSQLI_CLIENT_SSL, with the same non-SSL fallback on failure. Gated on the constant, so deployments that don't define a CA bundle keep identical non-SSL behavior (no-op) — verified against ORB/ORT and ETOM, which are unaffected.
  • Fixed the Api::dbappconnect() per-app routing guard: it compared the main connection's statics ($dbUser/$dbName/$dbHost) instead of the app statics ($dbUserApp/$dbNameApp/$dbHostApp), so the "already connected to this app" short-circuit never fired. Corrected the comparison and added DBI::closeApp() when the app target changes, so in-process app switching reconnects to the right tenant DB instead of reusing the previous app's cached handle. Latent cross-tenant hazard, previously masked by single-app-per-request + the cron worker's fork/reconnect() (which clears the app handle).
  • Removed dead method DBI::dbInitApp() — it could never run (called non-existent setCharsetApp()/setEngineApp()), had no callers anywhere, and duplicated the live Api::dbappconnect() path.

Migration: Shipyard self-update moved from Lambda into a kyte-php cron worker — KYTE-#201

KyteShipyardUpdateController::new previously published current_version to SNS_KYTE_SHIPYARD_UPDATE and the kyte-lambda-update-shipyard Lambda did the work. That Lambda had two production bugs: mimetypes.guess_type() returned None for .map/directory entries → boto3 upload_file(ContentType=None) failed (source maps never uploaded), and its kyte_shipyard_cf env var pointed at a nonexistent distribution → the invalidation threw NoSuchDistribution and the dashboard served stale.

The update now runs out-of-band in a cron worker, not synchronously in the request. The download/extract/upload/invalidate routinely exceeds the ~100s Cloudflare non-enterprise request ceiling (and ALB idle timeouts), so a synchronous controller action would 524 on Cloudflare-fronted installs. Both Lambda bugs are fixed inherently.

  • New KyteShipyardUpdate model + table (migrations/4.12.0_shipyard_update.sql) tracks each request and its outcome (status pending→running→complete/failed, requested_version, deployed_version, files_uploaded/files_failed, cloudfront_invalidated, message).
  • KyteShipyardUpdateController::new does a fast inline CDN version check (~100ms) and returns "up to date" immediately when current; otherwise it enqueues a KyteShipyardUpdate row (status=pending) and returns it. The dashboard polls GET on the model by id for live status.
  • ShipyardUpdateWorker (src/Cron/ShipyardUpdateWorker.php, a CronJobBase job, interval 60s) claims the oldest pending row, downloads + extracts the stable kyte-shipyard.zip (ext-zip / ZipArchive), uploads every file via Kyte\Aws\S3::write() with an explicit per-extension Content-Type (JS→application/javascript, CSS→text/css; unknown types like .map are omitted rather than crashing — fixes bug #1), then invalidates the distribution from config KYTE_SHIPYARD_CF (fixes bug #2, best-effort). heartbeat() extends the lease during large uploads.
  • Idempotency, two layers: the controller refuses to enqueue while a row is pending/running (request dedup); the worker registers with allow_concurrent=0 (lease lock) and claims rows via a guarded pending→running UPDATE keyed on affected_rows()==1 (execution dedup).

Setup: run the migration, php bin/register-shipyard-update-job.php, and set KYTE_SHIPYARD_S3 / KYTE_SHIPYARD_CF (+ optional KYTE_SHIPYARD_REGION, default us-east-1) in config.php. New dependency: ext-zip. SNS_KYTE_SHIPYARD_UPDATE is no longer referenced (the Lambda + SNS topic are decommissioned in the final #201 step).

kyte-php v4.11.1

Choose a tag to compare

@github-actions github-actions released this 25 Jun 08:44
a4ce599

Fix: CloudFront invalidation CallerReference collision (direct/KYTE_USE_SNS=false path) — KYTE-#201

Kyte\Aws\CloudFront::createInvalidation() built its CallerReference as time().$distributionIdsecond precision. Any two invalidations against the same distribution within the same second reused that reference with a different path batch, which CloudFront rejects with InvalidArgument. The wrapper then swallowed it as a generic "Unable to create new invalidation". This made the direct invalidation path (KYTE_USE_SNS=false) flaky under rapid or bulk publishes — a/the reason invalidation was historically routed through SNS→Lambda instead.

Fix: append uniqid('', true) (microsecond entropy) so successive CallerReferences never collide, and surface the real AWS error message instead of the generic one. Measured latency of a single createInvalidation is ~150–190 ms, so the synchronous direct call is well within request budgets (the old "timeout" symptom was this failure, not the call duration). Unblocks the #201 move off SNS for cache invalidation: fix lands first, then installs flip KYTE_USE_SNS=false, then the dead SNS branches get removed.

Best-effort invalidation hardening. With KYTE_USE_SNS=false the invalidation runs synchronously inside the publish request, so a transient CloudFront error (throttling, a missing distribution) would otherwise fail a publish whose content already wrote to S3 successfully. Wrapped all 10 invalidation sites (KytePageController ×3, KyteScriptController ×2, KyteLibraryController ×2, KytePageDataController, NavigationController, SideNavController) in best-effort try/catch — log and continue, never fail the publish — matching the pattern ApplicationController already used.

kyte-php v4.11.0

Choose a tag to compare

@github-actions github-actions released this 11 Jun 13:13
bd2cbe2

Feature: JWT-mode anonymous/public API access (AppContextStrategy) — KYTE-#229

Lets a site running in JWT auth mode (endpoint + appId, no embedded HMAC key/secret) serve requireAuth=false controllers to anonymous visitors (public/catalog browsing before any login) — something only HMAC mode could do before. Server-side half of the two-repo change (the kyte-api-js anonymous fall-through ships alongside).

New AppContextStrategy (src/Core/Auth/AppContextStrategy.php), slotted in AuthDispatcher::buildDefault() after JwtSessionStrategy and before HmacSessionStrategy:

  • matches() is strict and header-only — claims a request only when an x-kyte-appid is present and there is no Authorization Bearer, no x-kyte-signature, and no x-kyte-identity. Mutually exclusive with every authenticated flow, so it cannot shadow HMAC or JWT.
  • preAuth() resolves the application's account for query scoping but never resolves a user and never sets hasSession. That is the security invariant: ModelController::authenticate() throws unless both $api->user->id and $api->session->hasSession are set, so every requireAuth=true controller keeps returning 403 to anonymous requests — only requireAuth=false controllers are reachable.

Defense in depth:

  • Per-app tri-state opt-in. New Application.allow_public flag (default 0; migration migrations/4.11.0_application_allow_public.sql):
    • 0 (default) — anonymous appid-only requests are rejected in preAuth(); anonymous access is never implicit.
    • 1read-only: ModelController restricts an app_context request to GET regardless of the controller's allowableActions (public catalog/storefront browsing).
    • 2controller-governed: the controller's own requireAuth=false + allowableActions declaration governs, including writes — needed for pre-login flows like password reset (new/update are POST/PUT). This matches the contract controller authors have always written against: under HMAC, anonymous visitors to a public site can already reach every requireAuth=false action (the signing endpoint mints anonymous signatures from the embedded public key alone), so 2 exposes nothing HMAC does not. requireAuth=true controllers still 403 in every mode (the no-user/no-hasSession invariant is independent of allow_public).
  • Shadow harness. AuthShadowHarness skips app_context (no legacy equivalent to diff against during dispatcher rollout).

Audit attribution for anonymous requests uses user_id=null / session '0' (ActivityLogger already tolerates a null user). Existing HMAC and JWT-Bearer flows are unchanged. Tests: tests/AppContextStrategyTest.php (matches() truth table; preAuth resolves account but not user/hasSession; tri-state opt-in enforcement incl. unknown values treated as off).

Feature: platform-level password reset for JWT mode — /jwt/password-* (KYTE-#268)

Shipyard is platform-level (no x-kyte-appidapplicationId is deliberately null), so its anonymous password reset can ride neither HMAC anonymous (gone in JWT mode) nor AppContextStrategy (appid required). Result: password reset was broken on JWT-mode Shipyard installs. Fixed the same way /jwt/login solved appid-less login — dedicated unauthenticated endpoints on JwtEndpoint:

  • POST /jwt/password-reset {email} → always {ok:true} (no-reveal); known email gets a timestamped token (raw, in the password column — login disabled while pending) + SES mail.
  • POST /jwt/password-validate {token}{valid:bool, email} (password.html pre-check; the email is only disclosed to a live-token holder, who received it at that inbox).
  • POST /jwt/password-update {token, password} → consumes the token, stores the new password hashed, and revokes every refresh-token family for the user (a reset invalidates all sessions); 401 invalid_token on expired/unknown.

Token/email mechanics are extracted to Kyte\Core\Auth\PasswordResetFlow and shared with KytePasswordResetController (behavior-identical refactor — same token format, 1-hour TTL, no-reveal logging), so the HMAC/app-scoped path and the JWT platform path cannot drift. App-scoped sites keep using their own reset controllers via app_context mode 2. kyte-shipyard reset.js/password.js call the new endpoints when in JWT mode (ships in the Shipyard release alongside).

Tests: tests/JwtEndpointTest.php gains the /jwt/password-* suite (no-reveal, pending-token login lockout, validate/consume round-trip, refresh-family revocation, expired/unknown → 401).

kyte-php v4.10.1

Choose a tag to compare

@github-actions github-actions released this 11 Jun 13:12
a84430e

Fix: MCP commit_draft published raw bzip2 bytes into page HTML (header/footer section CSS not decompressed)

Publishing a page through the MCP commit_draft flow could inject raw bzip2-compressed binary (magic BZh9…) into the published HTML, inside <style>/<footer> blocks, producing a browser UTF-8 decode error and garbled header/footer CSS. Surfaced on FrameVTO / doctor.etometry.com (page 58, published to v13).

Root cause. A page's header/footer are FKs to KyteSectionTemplate, which stores html/stylesheet/javascript/block_layout bzip2-compressed. getObject()'s FK expansion returns those fields RAW. The page-assembly path (createHtmlbuildHeaderFooterStyles etc.) concatenates them straight into the output, so they must be decompressed first. The human publish path (HTTP update, state=1) was saved only incidentally — KytePageController::hook_response_data() runs first and decompresses $r['header']/$r['footer']. The MCP commit path (DraftService::commitDraftpublishForSurfaceKytePageController::publishFromContent) calls getObject() and goes straight to publishPage(), never invoking that hook — so the compressed bytes shipped. Deterministic per-path (not a race): any page with a populated header/footer section template was affected; the "re-publish cleared it" report was a re-publish via the human path.

Fixes (KytePageController, S3):

  1. Path-independent decompression. New decompressSectionTemplate(&$section) decompresses header/footer content via Bz2Codec::decompressIfBz2, called from BOTH hook_response_data() and publishFromContent(). The page's own html/stylesheet/javascript were already correct (decompressed by DraftService::versionContent); only the section templates leaked.
  2. Latent guard bug fixed. The old hook_response_data block required all four fields (html, stylesheet, javascript, block_layout) to be set or it decompressed none — a null block_layout would have leaked the other three even on the HTTP path. The new helper decompresses each field independently.
  3. Output integrity guard. publishPage() now runs hasBinaryContamination() on the assembled HTML (bzip2 stream magic BZh[1-9]1AY&SY, or invalid UTF-8) and aborts before the S3 write (returns false → MCP commit reports committed:false, draft left intact) rather than shipping corrupt HTML.
  4. Charset. Published HTML objects are now written with Content-Type: text/html; charset=utf-8 (S3::write() gained an optional content-type passed through the stream-wrapper context; other callers unchanged).

Tests: tests/PublishIntegrityTest.php covers the per-field decompression (incl. the null-block_layout regression) and the contamination detector (embedded bzip2 stream, invalid UTF-8, and no false-positive on literal "BZh" prose).

kyte-php v4.10.0

Choose a tag to compare

@github-actions github-actions released this 01 Jun 08:55
4616606

Feature: MCP draft/write — AI can draft and commit pages, controller functions, and scripts

Phase 2 write tools for the MCP server. Until now the MCP tools were read-only; this release lets an AI client propose changes as drafts and commit them live, across all three content surfaces — without ever touching the live resource until an explicit commit.

Model. A draft is a pending version row (draft=1, is_current=0) on the existing version tables — the live content and the current version are untouched until commit, which flips the draft to is_current=1 and publishes. A new migration migrations/4.10.0_version_draft_flags.sql adds draft + draft_source to KytePageVersion, KyteFunctionVersion, and KyteScriptVersion (additive, migration-first, inert on older code).

Engine. Kyte\Mcp\Service\DraftService is surface-generic, driven by a per-surface descriptor (version model, content model, parent model, content fields), reusing the same sha256 content-hash + bzip2 + reference_count dedup conventions as the existing controllers so drafts de-duplicate against existing version content.

Tools (Kyte\Mcp\Tools\DraftTools):

  • Writes (require draft scope): write_page_part(page_id, part, content), write_function_code(function_id, code), write_script_content(script_id, content). Repeated writes on the same resource accumulate into one open draft.
  • Review (require read): list_drafts(application_id) spans all surfaces (each row tagged with its surface); read_draft(surface, draft_id) returns content + which parts differ from live.
  • Lifecycle: discard_draft(surface, draft_id) (draft scope); commit_draft(surface, draft_id) (commit scope) — the only action that changes the live resource. Pages/scripts publish to S3 + invalidate CloudFront; functions write the live code and regenerate the controller's compiled code base. On a failed publish, commit returns committed:false + an error and leaves the draft intact (publishes first, so a failure never half-applies).

Supporting changes. KytePageController::publishPage is now public static and returns the S3 write result (existing void callers unaffected); it gains publishFromContent() for commit. KyteScriptController gains publishFromContent(). ModelController gains an optional internal constructor flag that skips the session authenticate() check, so a trusted server-side caller (the MCP commit flow) can use a controller with an account context but no HTTP session.

Token scopes (read / draft / commit) on KyteMCPToken already existed; new tokens stay draft-only by default, so committing live is opt-in per token. Sequenced by risk: pages and scripts (fully versioned) and controller functions; model/schema drafting is intentionally out of scope (deferred to the data-model initiative).

kyte-php v4.9.0

Choose a tag to compare

@github-actions github-actions released this 01 Jun 05:45
796f3e8

Fix: ActivityLogger no longer bloats KyteActivityLog, and the admin log list stops dragging blobs (KYTE-#182)

KyteActivityLog grew to 10GB on one install and OOM'd the admin log query. Two independent causes, both fixed here — pure code, no schema change, instant rollback.

  1. Write-cap (the durable bloat fix). request_data and changes are LONGTEXT and ActivityLogger stored the full json-encoded request body / diff. A single page or script save carries 300KB+ of HTML/JS/CSS, so each logged row could be hundreds of KB — multiplied across every write, that is what filled the table. ActivityLogger::log() now caps each of those two fields at KYTE_ACTIVITY_LOG_MAX_FIELD_BYTES (default 16384 bytes, auto-defaulted in Api). When the encoded value exceeds the cap it is replaced with a small audit-preserving marker — {"_truncated":true,"_original_bytes":N,"_fields":[...]} — so you still see what was sent or changed (the top-level field names and the original size), just not the megabyte of content. Set the constant to 0 to disable the limit. Redaction (SensitivityPolicy + the hardcoded SENSITIVE_FIELDS baseline) is unchanged and still runs before the cap.

  2. List-view projection (the OOM fix). The framework SELECT * (DBI::select) pulls both LONGTEXT columns for every row, and the admin log list never uses them — only the single-record detail view does. KyteActivityLogController now detects a by-id detail fetch in hook_prequery and, on every other (list) response, omits request_data/changes and skips the JSON decode. The detail view (Shipyard get("KyteActivityLog","id",idx)) still returns the full decoded payload, so no Shipyard change is required. Severity/action colors are still added to both list and detail rows.

Not in this release (deferred to the data-model initiative, KYTE-#190): index coverage on KyteActivityLog's filter/sort columns and a retention policy. The earlier one-off ETOM purge (10.4GB → 1.56GB) was the band-aid; the write-cap above is what prevents recurrence.