v6.1.0-edge.7: Field-Level Security, native tool calling, and AI Personas
Field-Level Security lands — per-field Read, Update and Create control by role, enforced server-side with no admin exemption — alongside a provider-neutral native tool-calling surface that ships switched off, an AI Persona catalog with modality inheritance, and GPT-Live realtime voice over WebRTC and SIP. Fixes across PostgreSQL conversion, Explorer search and CodeGen ride along.
Upgrade Notes apply: roughly 2,150 generated GraphQL fields lose non-nullability, so external consumers holding generated types must regenerate them.
Edge builds are prereleases. They publish under the edge dist-tag and never move latest.
New Features
Section titled “New Features”-
Field-Level Security — per-field Read/Update/Create control by role (
@memberjunction/core,@memberjunction/server,@memberjunction/core-entities,@memberjunction/codegen-lib,@memberjunction/generic-database-provider,@memberjunction/graphql-dataprovider,@memberjunction/sql-dialect). A newEntityFieldPermissiontable holds one row per (field, role) with three independent verbs, eachAllow,DenyorNo Access.No Accessis the neutral default;Denybeats any number ofAllows across a user’s roles. Field security is switched on per entity, explicitly, viaEntity.EnableFieldLevelSecurity— nothing is inferred from whether permission rows happen to exist, so adding a rule can never change access on an entity that has not opted in. Turning the flag on snapshots existing entity-level permissions into per-field rows, so enabling changes nothing until an administrator tightens a specific field; turning it off keeps the rows inactive rather than discarding the configuration. Read is required for Update and Create, enforced both by a CHECK constraint within a row and by clamping again across roles, because two individually legal rows held by one user would otherwise aggregate to write-only access no constraint could see. Enforcement is server-side and authoritative: denied columns are stripped from RunView results on both cache paths and from single-record GraphQL responses; a request naming a denied field inExtraFilter,OrderByor anAggregatesexpression is rejected, sinceMIN(Salary)reads the values back without the column ever appearing; saves that change an unreadable field are rejected and values for unreadable fields are ignored; a value supplied for a field the user may not create is dropped so the column takes its default, because an error naming the field would confirm it exists and is restricted. Nobody is exempt — no admin bypass, no Owner carve-out, and no special case for the MJ system user; what is protected instead is the configuration, which refuses a rule that denies anything to a role that account holds. Primary keys,__mj_columns and the security entities can never be restricted.MJ: Record Changesrows are projected against the entity each row is about, resolved per row, so an audit payload cannot be used to read a denied field out the side;ChangesDescriptionis withheld entirely rather than redacted, because prose leaks on the first value that appears in an unexpected form. New guide:guides/FIELD_LEVEL_SECURITY_GUIDE.md. -
NOT NULL columns can now be restricted (
@memberjunction/core,@memberjunction/server,@memberjunction/codegen-lib,@memberjunction/graphql-dataprovider,@memberjunction/ng-entity-viewer). The guide previously said not to restrict a NOT NULL column, which put roughly 2,150 of 4,650 restrictable fields off-limits — including the ~400 foreign-key display columns that inherit non-nullability from the key they display, so “hide which client this contract belongs to” did not work. The cause was one wrong inference: a column’s NOT NULL constraint and a GraphQL!say different things — “no row stores an empty value here” versus “every response, to every caller, carries a value here” — and the second does not follow from the first. They coincided only while every caller saw every column of every row they could read, which is exactly what field security ends. Generated object types are now non-nullable only where field security is structurally incapable of stripping a value. This removes three symptoms, all of which required a denied NOT NULL column: single-record loads nulling the entire record, typed list queries nulling the entire query, and a mutation whose write landed in the database while its response failed to serialize, so the client reported a failed save for an edit that had actually succeeded.ReadableFields___is added to every generated object type, because GraphQL emits every field the client selected — so a denied field that was asked for arrives as an explicitnullindistinguishable from a genuine one, and the client’s own metadata is stale in the window after a permission change. It lists readable fields rather than denied ones deliberately: naming denied fields would hand back precisely what metadata filtering exists to withhold. -
A provider-neutral native tool-calling surface on
BaseLLM, shipped switched off (@memberjunction/ai,@memberjunction/ai-anthropic,@memberjunction/ai-openai,@memberjunction/ai-gemini,@memberjunction/ai-cerebras,@memberjunction/ai-groq,@memberjunction/ai-prompts,@memberjunction/ai-agents). MJ’s providers have described agent actions as prose in the system prompt and parsed a JSON envelope back; newer agentically-trained models increasingly fight that, and the prose action catalog is most of a 56–104KB Loop system prompt.ChatParamsgainstools,toolChoiceandparallelToolCalls; responses normalize toChatCompletionMessage.toolCallswith parsed arguments plus a'tool_calls'finish reason, and multi-turn tool use round-trips through a newtoolmessage role. Nothing in the tool-calling path changes behavior on merge. The catalog now records which models and vendor servings can do native tool calling — 139 model rows and 83 vendor-level overrides — but every one carriesDefaultToNativeToolCalling: false, no prompt setsUseNativeToolCalling, and no MJ caller passes tools, so the gate resolves false on every existing path. The OpenAI-compatible mapping lives once in@memberjunction/aiand is shared by OpenAI, Groq and Cerebras, which matters because GPT-OSS-120B on Cerebras is the most-deployed model across MJ’s shipped agents and both drivers previously reportedSupportsTools: false, making the one configuration the feature most needed to reach the one it could not. A full-corpus comparison ran both arms four times at N=3 and returned NO-GO on all three providers: native tool calling delivers the gain it should — given that an action is the right answer, GPT 5.6-luna acts 76% of the time through the envelope and 96% with tools declared — but spends it on worse tool selection and on calling a tool on 5–20% of turns whose right answer was chat or delegation. The prompt-size premise did not hold on any run. Capability is a statement of fact and is safe to ship; policy is the switch, and it is off. -
AI Personas: a metadata catalog and resolution across the engine (
@memberjunction/core-entities,@memberjunction/ai-engine-base,@memberjunction/aiengine,@memberjunction/ai-agents,@memberjunction/server). Four new tables —AIPersona,AIPersonaVendor,AIModelPersona,AIAgentPersona— carry a curated cross-modality persona catalog, concrete vendor and modality bindings with typed vendor settings, model availability and priority sequences, and per-agent assignments with a filtered unique index enforcing one default per agent.BaseAIEnginecaches and resolves them (GetModelPersonas,GetAgentPersonas,ResolveAgentPersona), incorporating agent style overrides and sequence ordering, andGetRealtimeModelVoicesconsults metadata personas before falling back to a driver’sSupportedVoices. Alongside it, modality inheritance resolves Agent → Model → System → Default, honouringInheritTypeModalities, the model type’s default input and output modalities, and junction-levelIsSupported = 0/IsAllowed = 0vetoes. -
GPT-Live realtime voice, on the server and in the browser (
@memberjunction/ai-openai,@memberjunction/ai-realtime-client,@memberjunction/ai-bridge-base,@memberjunction/ai-bridge-server,@memberjunction/ai).OpenAILiveRealtimeconnects to OpenAI’s Live sessions endpoint with a remote reasoning plane that delegates to Responses, a task-revision counter giving supersede-and-discard cancellation so a stale in-flight result is discarded rather than spoken, and directaudio/pcmuG.711 μ-law carrier passthrough that removes PCM conversion and its odd-byte truncation.OpenAILiveClientis the browser half: it creates the events data channel before the offer, uses non-trickle ICE, exchanges SDP through a server broker, and accounts for the provider’s 15-second session pre-bill in its duration and usage reporting.OpenAISipBridgeandBaseDetachedMediaBridgesupport carriers whose media never transits MJ — audio in/out requirements become conditional on a detached media plane, and the transport seam skips relay loops while still flushing on barge-in. Realtime capability metadata expands to describe reasoning planes, delegation reconfiguration, interruption and cutoff signals, and usage bases; Inworld and ElevenLabs migrate to resolving their underlying model and managed agent throughReasoning.Remote.Ref. -
Realtime co-agents can invoke actions directly, gated by driver capability (
@memberjunction/ai-agents,@memberjunction/ng-conversations). A realtime agent could previously only delegate; it now projects a configured allowlist of actions into the session’s tool set and executes them through the Actions API with a timeout. The configuration is closed by default and names actions explicitly (or['*']), and the tools are only offered to drivers declaringSupportsDynamicToolSet— OpenAI, GPT-Live and xAI receive them; ElevenLabs and Gemini declare nothing and receive none. Direct action turns persist and render as review cards. -
Server Extension lifecycle: pre-auth and post-auth phases, and a typed service registry (
@memberjunction/server-extensions-core,@memberjunction/server,@memberjunction/messaging-adapters,@memberjunction/telephony-adapters). Extensions declare a phase and mount either before the authentication middleware — which is what a webhook needs in order to verify a signature — or after it. Extensions are refused a route that would shadow a reserved system root (/graphql,/auth,/health,/media,/schema,/mcp), anOnAllExtensionsMountedhook is awaited across every loaded extension once all phases have mounted, and aServerExtensionServiceRegistrylets extensions discover one another’s services without importing each other. The Twilio, Vonage, RingCentral and Teams adapters move out of MJServer into@memberjunction/telephony-adaptersas extensions on that lifecycle, leaving MJServer vendor-generic; legacyconfigInfo.telephonysettings are synthesized into extension config, so existing deployments need no change. New guide:guides/SERVER_EXTENSIONS_GUIDE.md. -
A skill can bundle an action without putting it into the agent’s run (
@memberjunction/ai-agents,@memberjunction/ai-engine-base,@memberjunction/aiengine,@memberjunction/core-entities,@memberjunction/server). Bundling an action into a skill made it visible and executable to the model for the rest of the run. A skill whose reply carries a menu — buttons the application wires to an action, pressed by the person on the next turn — wants the association without the model calling the action on its own mid-conversation (#4226).AISkillAction.ExposeToModeldefaults to1, today’s behaviour;0keeps the action bundled for SKILL.md export and tooling but out of the run. SKILL.md round-trips the flag through an optionalcodeOnlyActionsfrontmatter list.
Improvements
Section titled “Improvements”-
PostgreSQL identifier auto-quoting: 45 of 53 common constructs were corrupted (
@memberjunction/sql-dialect). A generated query against a PostgreSQL client usedCURRENT_DATEin a date predicate and came back ascolumn "CURRENT_DATE" does not exist— these functions are spelled without parentheses, so the word-before-(rule never classified them as functions. Auditing the class found the gap was broad, and every instance hinges on a word not followed by(. Added: the niladic datetime and identity functions; ordered-set aggregate and window-frame words, of whichWITHINis sharpest becausePERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x)is how every median is written; the rest of PostgreSQL’s reserved words, which are unconditionally safe because PostgreSQL will not resolve a same-named column bare either; type names in cast position, whereDOUBLE PRECISIONworked only because both halves happened to be listed andCHARACTER VARYINGdid not; and the utility statement verbs. Seventeen non-reserved words are deliberately excluded, since they are legal bare column names in a customer schema this repo’s baseline cannot see. The reverse guard in the baseline test was itself sampling its reserved-word oracle, which is why the gap went undetected; it is now transcribed in full. -
Applying a
MaxRowscap no longer rewrites the caller’s SQL on PostgreSQL (@memberjunction/generic-database-provider). The cap was injected by re-emitting the entire statement through the AST, andnode-sql-parsernormalizes as it generates — soORDER BY x ASC nulls lastcame back upper-cased, the identifier auto-quoter then quoted the now-capitalised keywords it did not recognise, and PostgreSQL rejectedORDER BY x ASC "NULLS" "LAST". The round-trip also rewrotecp."recordKey"and::integer. Where a trailingLIMIT Nis provably equivalent to the AST injection, the cap is appended as text and the statement is left byte-identical; every other shape falls through unchanged. Deliberately not switched to the existing wrapping fallback, which would put the cap above a subquery’sORDER BYwhere PostgreSQL does not guarantee the inner ordering survives — a “top 100 by rank” query could return an arbitrary 100. -
Three PostgreSQL conversion defects, each producing a file the converter called clean and PostgreSQL then rejected on apply (
@memberjunction/sql-converter,@memberjunction/sqlglot-ts). BIT literals were coerced insideINSERT … VALUESbut not inUPDATE … SETorWHERE, so a CodeGen update against a core-metadata table failed withoperator does not exist: boolean = integer. A comma inside a CodeGen comment shifted every subsequent column, because the value splitter tracked quoted strings and nested parens but not SQL comments — observed as a boolean landing in the integerScalecolumn. And PostgreSQL refuses to index a table carrying pending trigger events, which a migration hits whenever it seeds FK-bearing rows and then indexes the referenced table; the dialect now emitsSET CONSTRAINTS ALL IMMEDIATEahead of a guardedCREATE INDEX, at statement level, since issuing it inside theDOblock does not clear events queued by earlier statements. -
In-process CodeGen guards (
@memberjunction/codegen-lib). An AI-generated entity name is validated before use; a failed entity INSERT propagates instead of being swallowed, so the surrounding transaction can roll back rather than commit a partial set of new entities; a rolled-back batch no longer leaves its names in the process-static new-entity list; and in-process runs keep advanced generation off unless explicitly enabled (#4421). -
mj dev workspacesees Open App client packages and their shell peers (@memberjunction/cli). The generated parent manifest now registers everypackages.client[]andpackages.shared[]entry from each member’s committedmj-app.jsonatworkspace:*, unmet shell-provided peers are reported per shell with the version the parent already pins, anddoctorgains a check that a member’s declared client packages are linked at the parent. Fixes a page-load resolve failure that presented with a completely green build (#4364). -
Knowledge Hub and universal search fixes, found wiring a Dropbox team-space vault into Pinecone (
@memberjunction/storage,@memberjunction/content-autotagging,@memberjunction/search-engine). The Dropbox refresh-token constructor path now marks the driver configured, so callers that only construct it are no longer rejected with “Missing: Access Token”, and an optional path root lets Business team-space paths resolve. Cloud-storage autotagging walks sub-folders under its prefix instead of one level. Both the vectorizer and the search provider address third-party indexes byVectorIndex.ExternalIDrather than the MJ display name — the semantic lane had been 404ing against Pinecone on every query whenever the two differed. Invalid-content deletions and failed content-item saves are logged rather than silent. -
Weekly AI model and vendor refresh (2026-09-13) (
@memberjunction/ai,@memberjunction/aiengine,@memberjunction/core-entities). The GLM-5.3-Flash launch promo expired on schedule, so its cost row is nowExpiredwith anEndedAtand a new row records the list rate from that same instant — preserving the historical rate rather than overwriting it. GPT-6 Astra gains an Amazon Bedrock inference row and its short-context cost record.DeepSeek V4.1 Flashis added at its off-peak tier, recording a real tier rather than a blend. No OpenRouter cost row was written for it: the route exists but its rate was not confirmed, and an invented price is worse than an absent one.
Bug Fixes
Section titled “Bug Fixes”-
Explorer view and grid search returned 0 rows — or refused outright — for ordinary terms (
@memberjunction/server,@memberjunction/generic-database-provider).UserSearchStringis the free text a person typed into a search box, not a SQL clause, and it never reaches SQL as a fragment. Two screens intended for genuine fragments were applied to it anyway. The GraphQL-boundary AST screen wraps its argument as aWHEREclause and fails closed when that does not parse, soMarcus Chenparsed as nothing andO'Learyas an unterminated literal — which made essentially every person-name search return nothing. The provider’s keyword denylist, word-boundary-matched against free text, refusedUnion Pacific,Update Requestanddrop shipment, and the error reached the grid with a null message so the search box simply looked broken. Both screens are removed from this one input; every genuine clause fragment is screened exactly as before. One exception is retained: a field carrying a custom search format may place the term outside quotes, so the denylist is re-applied there, and only when such a field will actually participate (#4392). -
An RLS row filter was collected from permission rows that did not grant the operation (
@memberjunction/core). The filters of a user’s roles are OR’d together, so a filter read off a row whoseCan*flag was false widened the clause: a user granted Create by role A bound to filter F1 created againstF1 OR F2whenever role B kept a leftoverCreateRLSFilterID = F2besideCanCreate = false. Nothing clears the filter column when the flag is cleared, and the flags are OR’d across roles, so such a user passed the permission gate on role A alone. Granting rows are unchanged. -
A save refused by a server-side
ValidateAsync()now highlights the offending fields (@memberjunction/global,@memberjunction/core,@memberjunction/server,@memberjunction/graphql-dataprovider,@memberjunction/ng-base-forms). An asynchronous refusal only raised a toast, while a synchronousValidate()refusal drew the red border and inline message. Write refusals now carry structured validation errors that the provider rehydrates and the form publishes through one path, and a server-reported error stays visible on a field the user had already edited until they edit it again. A browser-sideValidateAsync()benefits too — its refusal previously surfaced as a bare “Error saving record”. Errors with no field source stay toast-only. -
Every integration rig that imported the harness shim died before running a single assertion (
@memberjunction/integration-test-suite,@memberjunction/testing-integration). The shim re-exported two symbols the framework package does not export — they live in the suite package, since the framework ships content-free by design — and a named re-export of a binding the source module does not provide is an ESM link-time error. It therefore failed at module load rather than where it was written, taking down the nightly cross-server invalidation rig plus five hand-run rigs, andtscnever saw it because the rigs directory sits outside the package’s tsconfig. Nothing consumed either symbol, so they are removed rather than re-pointed, and a test now checks each forwarded name against the package’s real exports. -
Deny rows can no longer be read as grants by the RLS readers (
@memberjunction/core,@memberjunction/materialization,@memberjunction/codegen-lib).EntityPermissionInfo.IsDenyis now one predicate — case- and whitespace-insensitive, with a blank type treated as Allow — used byGetUserPermisionsand by both RLS readers, so a setCan*flag on a Deny row is never mistaken for a grant. Unreachable in practice, since a user carrying a Deny row fails the permission gate first, but the methods now implement the invariant their documentation states. -
The input dialog’s body no longer sits flush against the dialog edges (
@memberjunction/ng-conversations). The dialog container pads only string content, so the message, labels and inputs of a component body went unpadded while the header and footer did not — visible in rename-conversation and every otherdialogService.input()prompt. -
Deleting the last
codeOnlyActionsentry from a SKILL.md now takes effect (@memberjunction/ai-agents). A known list key in block form with no items parsed as an absent key rather than an explicit empty list, so removing the final name and re-importing silently carried the old code-only flag instead of putting the action back into the agent’s run. Warnings for that key now quote the name the author typed rather than the resolved GUID.
Upgrade Notes
Section titled “Upgrade Notes”-
Generated GraphQL object types lose non-nullability on roughly 2,150 of 4,650 restrictable fields, across all 384 generated object types.
String!becomesString, and likewise for the other scalars. Any external consumer holding GraphQL types generated against the previous schema will fail to compile against this one until those types are regenerated; a consumer that reads the fields without regenerating sees no runtime change. Input types are not affected, so no write contract changes. Non-nullability is retained only on primary keys and__mj_system columns, where field security is structurally incapable of stripping a value. -
Full-text search joins multi-word terms differently. For entities with full-text search enabled, boolean-operator detection is now word-boundary based rather than substring based. Previously
ORmatched inside “corporate” andANDinside “standard”, so ordinary two-word searches were emitted with%joins —Corporate%Office, which is not valid full-text syntax. They are now joined asCorporate AND Office. Deployments relying on the previous%-joined output should re-check their full-text searches. -
Eight prompts change model once this release’s metadata is pushed. Query Builder and the Research Agent family — 7 agents, 8 prompts — move from
Gemini 3.5 FlashtoGemini 3.8 Flash. The Flash-Lite rows that outranked them are setStatus: "Inactive"rather than deleted, becausemj sync pushnever deletes and removing the rows from JSON would have left those agents on Flash-Lite in every existing database. -
Seven schema migrations ship in this release, covering the
EntityPermissionuniqueness constraint and its duplicate merge, identity-claim foreign-key index healing,EntityFieldPermission,AISkillAction.ExposeToModel, the AI Persona schema, native tool calling, and the consolidated metadata sync. TheEntityPermissionmerge refuses to run where duplicate rows name different row-level-security filters — two different filters have no defensible union, and choosing one would silently change which rows a role can see. It names the offending groups and asks for them to be resolved by hand.