chore: preserve paused QBit work and document remaining issues
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Failing after 7s
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Failing after 6s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Failing after 7s
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / ci-result (push) Failing after 6s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
Save the current implementation and README, status, and known-issue documentation at the user-requested stop. No completed release or deployment is claimed. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: GPT-6 Codex <noreply@openai.com>
This commit is contained in:
257 files changed
+47918
No files matched your search
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.scripting;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import com.kingsrook.qbits.scripting.service.IsolatedSourceGuard;
|
||||
import com.kingsrook.qbits.scripting.service.ScriptingService;
|
||||
import com.kingsrook.qbits.scripting.service.WarmRuntimeService;
|
||||
import com.kingsrook.qbits.scripting.worker.WarmWorkerMaintenance;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.MultiCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.ScriptTableCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.health.HealthCheckMetaData;
|
||||
import com.kingsrook.qqq.backend.core.health.HealthChecksMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProducer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Registers the scripting-owned tables (builds, artifacts, publications,
|
||||
** bindings, execution history) and creates the instance's ScriptingService. The
|
||||
** project and draft-source tables are the existing core script tables and must
|
||||
** already be defined. Every scripting table is read-protected by scripting.view
|
||||
** and writable only by the service itself (see ScriptingTablePermission).
|
||||
*******************************************************************************/
|
||||
public class ScriptingQBitProducer implements QBitProducer
|
||||
{
|
||||
private ScriptingQBitConfig config;
|
||||
private Consumer<QTableMetaData> tableEnricher = table ->
|
||||
{
|
||||
// default: nothing; RDBMS hosts add their backend details here
|
||||
};
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Supply the host configuration.
|
||||
***************************************************************************/
|
||||
public ScriptingQBitProducer withConfig(ScriptingQBitConfig value)
|
||||
{
|
||||
config = value;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Let the host add backend-specific table details, for example RDBMS table
|
||||
** names, to each scripting table.
|
||||
***************************************************************************/
|
||||
public ScriptingQBitProducer withTableEnricher(Consumer<QTableMetaData> value)
|
||||
{
|
||||
tableEnricher = value;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Validate prerequisites, add the tables, and install the service.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void produce(QInstance instance, String namespace) throws QException
|
||||
{
|
||||
if(config == null)
|
||||
{
|
||||
throw new QException("Scripting requires a configuration");
|
||||
}
|
||||
if(namespace != null && !namespace.isBlank() && !"scripting".equals(namespace))
|
||||
{
|
||||
throw new QException("The scripting QBit supports the scripting namespace only");
|
||||
}
|
||||
config.validate(instance);
|
||||
if(!instance.getTable("script").getFields().containsKey("executionMode"))
|
||||
{
|
||||
throw new QException("Scripting requires the reviewed core isolation marker metadata");
|
||||
}
|
||||
requireSystemReadableProjectLocks(instance);
|
||||
for(TableCustomizers hook : List.of(TableCustomizers.PRE_INSERT_RECORD, TableCustomizers.PRE_UPDATE_RECORD, TableCustomizers.PRE_DELETE_RECORD))
|
||||
{
|
||||
appendGuard(instance.getTable("script"), hook, new QCodeReference(ScriptTableCustomizer.class));
|
||||
appendGuard(instance.getTable("scriptRevision"), hook, new QCodeReference(IsolatedSourceGuard.class));
|
||||
appendGuard(instance.getTable("scriptRevisionFile"), hook, new QCodeReference(IsolatedSourceGuard.class));
|
||||
}
|
||||
for(String name : List.of("scriptRevision", "scriptRevisionFile"))
|
||||
{
|
||||
QTableMetaData source = instance.getTable(name);
|
||||
QPermissionRules rules = PermissionsHelper.getEffectivePermissionRules(source, instance).clone();
|
||||
source.setPermissionRules(rules.withCustomPermissionChecker(SourceAggregatePermission.wrap(rules.getCustomPermissionChecker())));
|
||||
source.withoutCapabilities(Capability.TABLE_COUNT, Capability.QUERY_STATS);
|
||||
}
|
||||
appendGuard(instance.getTable("scriptRevision"), TableCustomizers.POST_QUERY_RECORD, new QCodeReference(IsolatedSourceGuard.class));
|
||||
appendGuard(instance.getTable("scriptRevisionFile"), TableCustomizers.POST_QUERY_RECORD, new QCodeReference(IsolatedSourceGuard.class));
|
||||
|
||||
instance.addQBit(new QBitMetaData().withGroupId("com.kingsrook.qbits").withArtifactId("qbit-scripting").withVersion(version()).withNamespace("scripting").withConfig(config));
|
||||
|
||||
instance.addTable(table(ScriptingTables.BUILD, "Script builds",
|
||||
integer("script_id"), integer("script_revision_id"), string("source_sha256"), text("manifest_json"), string("manifest_attestation"), string("toolchain_id"), string("status"),
|
||||
text("diagnostics_json"), string("requested_by"), dateTime("created_at")));
|
||||
instance.addTable(table(ScriptingTables.ARTIFACT, "Script artifacts",
|
||||
integer("build_id"), integer("script_id"), integer("version"), string("source_sha256"), string("toolchain_id"), string("entry_point"), string("coordinator_id"),
|
||||
text("bundle"), string("bundle_sha256"), integer("size_bytes"), text("source_map"), string("attestation"), dateTime("created_at"))
|
||||
.withUniqueKey(new UniqueKey("build_id")).withUniqueKey(new UniqueKey("script_id", "version")));
|
||||
instance.addTable(table(ScriptingTables.PUBLICATION, "Script publications",
|
||||
integer("script_id"), integer("version"), integer("build_id"), integer("artifact_id"), string("published_by"), dateTime("published_at"), string("note"))
|
||||
.withUniqueKey(new UniqueKey("script_id", "version")));
|
||||
instance.addTable(table(ScriptingTables.BINDING, "Script hook bindings",
|
||||
string("name"), string("table_name"), string("event"), integer("publication_id"), text("grants_json"), text("patchable_fields"), integer("enabled"), string("active_slot"),
|
||||
string("created_by"), dateTime("created_at"))
|
||||
.withUniqueKey(new UniqueKey("active_slot")));
|
||||
instance.addTable(table(ScriptingTables.EXECUTION, "Script executions",
|
||||
string("operation_id").withMaxLength(64), string("kind"), integer("build_id"), integer("binding_id"), integer("publication_id"), integer("artifact_id"), string("actor_id"), string("event"),
|
||||
string("status"), integer("duration_ms"), string("message"), string("result_action"), dateTime("created_at")));
|
||||
instance.addTable(table(ScriptingTables.IMPORT, "Script source imports",
|
||||
integer("script_id"), integer("script_revision_id"), string("repository"), string("commit_sha"), string("subdirectory"),
|
||||
string("source_sha256"), string("imported_by"), dateTime("created_at"))
|
||||
.withUniqueKey(new UniqueKey("script_revision_id")));
|
||||
|
||||
ScriptingAuthoringProcesses.register(instance);
|
||||
ScriptingNavigation.register(instance);
|
||||
config.setService(new ScriptingService(config.getRuntime()));
|
||||
if(config.getRuntime().getWorker() instanceof WarmWorkerMaintenance)
|
||||
{
|
||||
instance.withRuntimeService(new QCodeReference(WarmRuntimeService.class));
|
||||
HealthChecksMetaData.ofOrWithNew(instance).withCheck(new HealthCheckMetaData().withName("scripting.warm")
|
||||
.withLabel("Scripting warm code").withComponent("scripting").withIncludeInReadiness(true)
|
||||
.withFailureThreshold(1).withIntervalSeconds(10).withTimeoutMs(5000).withProbeCache(false)
|
||||
.withCheck(new QCodeReference(WarmRuntimeService.class)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The QBit alone reads and writes its project tables as the QQQ system session,
|
||||
** after the caller has been authorized. A host may lock those tables with
|
||||
** native record security (for example by tenant); QQQ lets the system session
|
||||
** through only for a key type that defines an all-access key. Without it every
|
||||
** internal read would silently see no rows and even the owner's authoring would
|
||||
** fail as "not found", so refuse the configuration with the exact remedy.
|
||||
***************************************************************************/
|
||||
private static void requireSystemReadableProjectLocks(QInstance instance) throws QException
|
||||
{
|
||||
for(String tableName : List.of("script", "scriptRevision", "scriptRevisionFile"))
|
||||
{
|
||||
for(var lock : flatten(instance.getTable(tableName).getRecordSecurityLocks()))
|
||||
{
|
||||
var keyType = lock.getSecurityKeyType() == null ? null : instance.getSecurityKeyType(lock.getSecurityKeyType());
|
||||
if(keyType != null && (keyType.getAllAccessKeyName() == null || keyType.getAllAccessKeyName().isBlank()))
|
||||
{
|
||||
throw new QException("Scripting reads its project tables as the QQQ system session, but the record-security lock on table " + tableName
|
||||
+ " uses key type " + keyType.getName() + " which defines no all-access key. Configure QSecurityKeyType.withAllAccessKeyName for it.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Leaf locks of a possibly nested lock list.
|
||||
***************************************************************************/
|
||||
private static List<com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock> flatten(
|
||||
List<com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock> locks)
|
||||
{
|
||||
List<com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock> leaves = new java.util.ArrayList<>();
|
||||
for(var lock : locks == null ? List.<com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock>of() : locks)
|
||||
{
|
||||
if(lock instanceof com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock multi)
|
||||
{
|
||||
leaves.addAll(flatten(multi.getLocks()));
|
||||
}
|
||||
else
|
||||
{
|
||||
leaves.add(lock);
|
||||
}
|
||||
}
|
||||
return (leaves);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Keep host behavior and apply integrity checks to its final record values.
|
||||
***************************************************************************/
|
||||
private static void appendGuard(QTableMetaData table, TableCustomizers hook, QCodeReference guard)
|
||||
{
|
||||
QCodeReference prior = table.getCustomizer(hook.getRole()).orElse(null);
|
||||
if(prior == null)
|
||||
{
|
||||
table.withCustomizer(hook, guard);
|
||||
}
|
||||
else
|
||||
{
|
||||
table.getCustomizers().put(hook.getRole(), MultiCustomizer.of(prior, guard));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The build-produced JAR version; IDE/test class directories say so honestly.
|
||||
***************************************************************************/
|
||||
private static String version()
|
||||
{
|
||||
String version = ScriptingQBitProducer.class.getPackage().getImplementationVersion();
|
||||
return (version == null || version.isBlank() ? "development" : version);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** One scripting table: integer id, the given fields, all non-editable in the
|
||||
** UI, protected by the shared table permission rule.
|
||||
***************************************************************************/
|
||||
private QTableMetaData table(String name, String label, QFieldMetaData... fields)
|
||||
{
|
||||
QTableMetaData table = new QTableMetaData()
|
||||
.withName(name)
|
||||
.withLabel(label)
|
||||
.withBackendName(config.getBackendName())
|
||||
.withPrimaryKeyField("id")
|
||||
.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(ScriptingTablePermission.class))
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withCustomPermissionChecker(new QCodeReference(ScriptingTablePermission.class)))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withLabel("Id").withIsEditable(false));
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
table.withField(field);
|
||||
}
|
||||
tableEnricher.accept(table);
|
||||
return (table);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read-only string field with a readable label.
|
||||
***************************************************************************/
|
||||
private static QFieldMetaData string(String name)
|
||||
{
|
||||
return (field(name, QFieldType.STRING));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read-only long text field.
|
||||
***************************************************************************/
|
||||
private static QFieldMetaData text(String name)
|
||||
{
|
||||
return (field(name, QFieldType.TEXT));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read-only integer field.
|
||||
***************************************************************************/
|
||||
private static QFieldMetaData integer(String name)
|
||||
{
|
||||
return (field(name, QFieldType.INTEGER));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Read-only timestamp field.
|
||||
***************************************************************************/
|
||||
private static QFieldMetaData dateTime(String name)
|
||||
{
|
||||
return (field(name, QFieldType.DATE_TIME));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Build a field whose label is derived from its snake_case name.
|
||||
***************************************************************************/
|
||||
private static QFieldMetaData field(String name, QFieldType type)
|
||||
{
|
||||
String label = name.substring(0, 1).toUpperCase(java.util.Locale.ROOT) + name.substring(1).replace('_', ' ');
|
||||
return (new QFieldMetaData(name, type).withLabel(label).withIsEditable(false));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user