Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/JAVA/Openclaw/extensions/codex/src/app-server/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 22 kB image not shown  

Quelle  approval-bridge.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import {
  type AgentApprovalEventData,
  formatApprovalDisplayPath,
  type EmbeddedRunAttemptParams,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
  approvalRequestExplicitlyUnavailable,
  mapExecDecisionToOutcome,
  requestPluginApproval,
  type AppServerApprovalOutcome,
  waitForPluginApprovalDecision,
} from "./plugin-approval-roundtrip.js";
import { isJsonObject, type JsonObject, type JsonValue } from "./protocol.js";

const PERMISSION_DESCRIPTION_MAX_LENGTH = 700;
const PERMISSION_SAMPLE_LIMIT = 2;
const PERMISSION_VALUE_MAX_LENGTH = 48;
const APPROVAL_PREVIEW_SCAN_MAX_LENGTH = 4096;
const APPROVAL_PREVIEW_OMITTED = "[preview truncated or unsafe content omitted]";
const ANSI_OSC_SEQUENCE_RE = new RegExp(
  String.raw`(?:\u001b]|\u009d)[^\u001b\u009c\u0007]*(?:\u0007|\u001b\\|\u009c)`,
  "g",
);
const ANSI_CONTROL_SEQUENCE_RE = new RegExp(
  String.raw`(?:\u001b\[[0-?]*[ -/]*[@-~]|\u009b[0-?]*[ -/]*[@-~]|\u001b[@-Z\\-_])`,
  "g",
);
const CONTROL_CHARACTER_RE = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]+`, "g");
const INVISIBLE_FORMATTING_CONTROL_RE = new RegExp(
  String.raw`[\u00ad\u034f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff\ufe00-\ufe0f\u{e0100}-\u{e01ef}]`,
  "gu",
);
const DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE = new RegExp(
  String.raw`(?:\u001b\][^\u001b\u009c\u0007]*|\u009d[^\u001b\u009c\u0007]*|\u001b\[[0-?]*[ -/]*|\u009b[0-?]*[ -/]*|\u001b)$`,
);

type ApprovalPreviewSource = {
  value: string;
  clipped: boolean;
};

type SanitizedApprovalPreview = {
  text?: string;
  omitted: boolean;
};

export async function handleCodexAppServerApprovalRequest(params: {
  method: string;
  requestParams: JsonValue | undefined;
  paramsForRun: EmbeddedRunAttemptParams;
  threadId: string;
  turnId: string;
  signal?: AbortSignal;
}): Promise<JsonValue | undefined> {
  const requestParams = isJsonObject(params.requestParams) ? params.requestParams : undefined;
  if (!matchesCurrentTurn(requestParams, params.threadId, params.turnId)) {
    return undefined;
  }
  if (!isSupportedAppServerApprovalMethod(params.method)) {
    return unsupportedApprovalResponse();
  }

  const context = buildApprovalContext({
    method: params.method,
    requestParams,
    paramsForRun: params.paramsForRun,
  });

  try {
    const requestResult = await requestPluginApproval({
      paramsForRun: params.paramsForRun,
      title: context.title,
      description: context.description,
      severity: context.severity,
      toolName: context.toolName,
      toolCallId: context.itemId,
    });

    const approvalId = requestResult?.id;
    if (!approvalId) {
      emitApprovalEvent(params.paramsForRun, {
        phase: "resolved",
        kind: context.kind,
        status: "unavailable",
        title: context.title,
        ...context.eventDetails,
        ...approvalEventScope(params.method, "denied"),
        message: "Codex app-server approval route unavailable.",
      });
      return buildApprovalResponse(params.method, context.requestParams, "denied");
    }

    emitApprovalEvent(params.paramsForRun, {
      phase: "requested",
      kind: context.kind,
      status: "pending",
      title: context.title,
      approvalId,
      approvalSlug: approvalId,
      ...context.eventDetails,
      message: "Codex app-server approval requested.",
    });

    const decision = approvalRequestExplicitlyUnavailable(requestResult)
      ? null
      : await waitForPluginApprovalDecision({ approvalId, signal: params.signal });
    const outcome = mapExecDecisionToOutcome(decision);

    emitApprovalEvent(params.paramsForRun, {
      phase: "resolved",
      kind: context.kind,
      status:
        outcome === "denied"
          ? "denied"
          : outcome === "unavailable"
            ? "unavailable"
            : outcome === "cancelled"
              ? "failed"
              : "approved",
      title: context.title,
      approvalId,
      approvalSlug: approvalId,
      ...context.eventDetails,
      ...approvalEventScope(params.method, outcome),
      message: approvalResolutionMessage(outcome),
    });
    return buildApprovalResponse(params.method, context.requestParams, outcome);
  } catch (error) {
    const cancelled = params.signal?.aborted === true;
    emitApprovalEvent(params.paramsForRun, {
      phase: "resolved",
      kind: context.kind,
      status: cancelled ? "failed" : "unavailable",
      title: context.title,
      ...context.eventDetails,
      ...approvalEventScope(params.method, cancelled ? "cancelled" : "denied"),
      message: cancelled
        ? "Codex app-server approval cancelled because the run stopped."
        : `Codex app-server approval route failed: ${formatErrorMessage(error)}`,
    });
    return buildApprovalResponse(
      params.method,
      context.requestParams,
      cancelled ? "cancelled" : "denied",
    );
  }
}

export function buildApprovalResponse(
  method: string,
  requestParams: JsonObject | undefined,
  outcome: AppServerApprovalOutcome,
): JsonValue {
  if (method === "item/commandExecution/requestApproval") {
    return { decision: commandApprovalDecision(requestParams, outcome) };
  }
  if (method === "item/fileChange/requestApproval") {
    return { decision: fileChangeApprovalDecision(outcome) };
  }
  if (method === "item/permissions/requestApproval") {
    if (outcome === "approved-session" || outcome === "approved-once") {
      return {
        permissions: requestedPermissions(requestParams),
        scope: outcome === "approved-session" ? "session" : "turn",
      };
    }
    return { permissions: {}, scope: "turn" };
  }
  return unsupportedApprovalResponse();
}

function matchesCurrentTurn(
  requestParams: JsonObject | undefined,
  threadId: string,
  turnId: string,
): boolean {
  if (!requestParams) {
    return false;
  }
  const requestThreadId =
    readString(requestParams, "threadId") ?? readString(requestParams, "conversationId");
  const requestTurnId = readString(requestParams, "turnId");
  return requestThreadId === threadId && requestTurnId === turnId;
}

function buildApprovalContext(params: {
  method: string;
  requestParams: JsonObject | undefined;
  paramsForRun: EmbeddedRunAttemptParams;
}) {
  const itemId =
    readString(params.requestParams, "itemId") ??
    readString(params.requestParams, "callId") ??
    readString(params.requestParams, "approvalId");
  const commandPreview = sanitizeApprovalPreview(
    readDisplayCommandPreview(params.requestParams),
    180,
  );
  const reasonPreview = sanitizeApprovalPreview(
    readStringPreview(params.requestParams, "reason"),
    180,
  );
  const command = commandPreview.text;
  const reason = reasonPreview.text;
  const kind = approvalKindForMethod(params.method);
  const permissionLines =
    params.method === "item/permissions/requestApproval"
      ? describeRequestedPermissions(params.requestParams)
      : [];
  const title =
    kind === "exec"
      ? "Codex app-server command approval"
      : params.method === "item/permissions/requestApproval"
        ? "Codex app-server permission approval"
        : kind === "plugin"
          ? "Codex app-server file approval"
          : "Codex app-server approval";
  const subject =
    permissionLines[0] ??
    (command
      ? `Command: ${formatApprovalPreviewSubject(command, commandPreview.omitted)}`
      : commandPreview.omitted
        ? `Command: ${APPROVAL_PREVIEW_OMITTED}`
        : reason
          ? `Reason: ${formatApprovalPreviewSubject(reason, reasonPreview.omitted)}`
          : reasonPreview.omitted
            ? `Reason: ${APPROVAL_PREVIEW_OMITTED}`
            : `Request method: ${params.method}`);
  const description =
    permissionLines.length > 0
      ? joinDescriptionLinesWithinLimit(permissionLines, PERMISSION_DESCRIPTION_MAX_LENGTH)
      : [subject, params.paramsForRun.sessionKey && `Session: ${params.paramsForRun.sessionKey}`]
          .filter(Boolean)
          .join("\n");
  return {
    kind,
    title,
    description,
    severity: kind === "exec" ? ("warning" as const) : ("info" as const),
    toolName:
      kind === "exec"
        ? "codex_command_approval"
        : params.method === "item/permissions/requestApproval"
          ? "codex_permission_approval"
          : "codex_file_approval",
    itemId,
    requestParams: params.requestParams,
    eventDetails: {
      ...(itemId ? { itemId } : {}),
      ...(command ? { command } : {}),
      ...(commandPreview.omitted ? { commandPreviewOmitted: true } : {}),
      ...(reason ? { reason } : {}),
      ...(reasonPreview.omitted ? { reasonPreviewOmitted: true } : {}),
    },
  };
}

function commandApprovalDecision(
  requestParams: JsonObject | undefined,
  outcome: AppServerApprovalOutcome,
): JsonValue {
  if (outcome === "cancelled") {
    return commandRejectionDecision(requestParams, "cancel");
  }
  if (outcome === "denied" || outcome === "unavailable") {
    return commandRejectionDecision(requestParams, "decline");
  }
  if (outcome === "approved-session") {
    if (hasAvailableDecision(requestParams, "acceptForSession")) {
      return "acceptForSession";
    }
    const amendmentDecision = findAvailableCommandAmendmentDecision(requestParams);
    if (amendmentDecision) {
      return amendmentDecision;
    }
  }
  return hasAvailableDecision(requestParams, "accept")
    ? "accept"
    : commandRejectionDecision(requestParams, "decline");
}

function fileChangeApprovalDecision(outcome: AppServerApprovalOutcome): JsonValue {
  if (outcome === "cancelled") {
    return "cancel";
  }
  if (outcome === "denied" || outcome === "unavailable") {
    return "decline";
  }
  return outcome === "approved-session" ? "acceptForSession" : "accept";
}

function requestedPermissions(requestParams: JsonObject | undefined): JsonObject {
  const permissions = isJsonObject(requestParams?.permissions) ? requestParams.permissions : {};
  const granted: JsonObject = {};
  if (isJsonObject(permissions.network)) {
    granted.network = permissions.network;
  }
  if (isJsonObject(permissions.fileSystem)) {
    granted.fileSystem = permissions.fileSystem;
  }
  return granted;
}

function unsupportedApprovalResponse(): JsonValue {
  return {
    decision: "decline",
    reason: "OpenClaw codex app-server bridge does not grant native approvals yet.",
  };
}

function describeRequestedPermissions(requestParams: JsonObject | undefined): string[] {
  const permissions = requestedPermissions(requestParams);
  const lines: string[] = [];
  const kinds: string[] = [];
  const risks = new Set<string>();
  if (isJsonObject(permissions.network)) {
    kinds.push("network");
  }
  if (isJsonObject(permissions.fileSystem)) {
    kinds.push("fileSystem");
  }
  if (kinds.length > 0) {
    lines.push(`Permissions: ${kinds.join(", ")}`);
  }
  let networkSummary: string | undefined;
  if (isJsonObject(permissions.network)) {
    networkSummary = summarizePermissionRecord(permissions.network, risks, [
      {
        key: "allowHosts",
        label: "allowHosts",
        sanitize: sanitizePermissionHostValue,
        risksFor: permissionHostRisks,
      },
    ]);
  }
  let fileSystemSummary: string | undefined;
  if (isJsonObject(permissions.fileSystem)) {
    fileSystemSummary = summarizePermissionRecord(permissions.fileSystem, risks, [
      {
        key: "roots",
        label: "roots",
        sanitize: sanitizePermissionPathValue,
        risksFor: permissionPathRisks,
      },
      {
        key: "readPaths",
        label: "readPaths",
        sanitize: sanitizePermissionPathValue,
        risksFor: permissionPathRisks,
      },
      {
        key: "writePaths",
        label: "writePaths",
        sanitize: sanitizePermissionPathValue,
        risksFor: permissionPathRisks,
      },
    ]);
  }
  if (risks.size > 0) {
    lines.push(`High-risk targets: ${[...risks].join(", ")}`);
  }
  if (networkSummary) {
    lines.push(`Network ${networkSummary}`);
  }
  if (fileSystemSummary) {
    lines.push(`File system ${fileSystemSummary}`);
  }
  return lines;
}

type PermissionArrayDescriptor = {
  key: string;
  label: string;
  sanitize: (value: string) => string;
  risksFor: (value: string) => readonly string[];
};

function summarizePermissionRecord(
  permission: JsonObject,
  risks: Set<string>,
  descriptors: readonly PermissionArrayDescriptor[],
): string | undefined {
  const details: string[] = [];
  for (const descriptor of descriptors) {
    const summary = summarizePermissionArray(permission, descriptor, risks);
    if (summary) {
      details.push(summary);
    }
  }
  return details.length > 0 ? details.join("; ") : undefined;
}

function summarizePermissionArray(
  record: JsonObject,
  descriptor: PermissionArrayDescriptor,
  risks: Set<string>,
): string | undefined {
  const values = readStringArray(record, descriptor.key);
  if (values.length === 0) {
    return undefined;
  }
  for (const value of values) {
    for (const risk of descriptor.risksFor(value)) {
      risks.add(risk);
    }
  }
  const sampleValues = values
    .slice(0, PERMISSION_SAMPLE_LIMIT)
    .map(descriptor.sanitize)
    .filter(Boolean);
  if (sampleValues.length === 0) {
    return `${descriptor.label}: ${values.length}`;
  }
  const remaining = values.length - sampleValues.length;
  const remainderSuffix = remaining > 0 ? ` (+${remaining} more)` : "";
  return `${descriptor.label}: ${sampleValues.join(", ")}${remainderSuffix}`;
}

function readStringArray(record: JsonObject, key: string): string[] {
  const value = record[key];
  return Array.isArray(value)
    ? value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean)
    : [];
}

function sanitizePermissionHostValue(value: string): string {
  const compact = sanitizePermissionScalar(value).toLowerCase();
  const withoutScheme = compact.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
  const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? withoutScheme;
  const withoutUserInfo = authority.includes("@")
    ? authority.slice(authority.lastIndexOf("@") + 1)
    : authority;
  return truncate(withoutUserInfo, PERMISSION_VALUE_MAX_LENGTH);
}

function sanitizePermissionPathValue(value: string): string {
  return truncate(
    formatApprovalDisplayPath(sanitizePermissionScalar(value)),
    PERMISSION_VALUE_MAX_LENGTH,
  );
}

function sanitizePermissionScalar(value: string): string {
  return sanitizeVisibleScalar(value);
}

function permissionHostRisks(value: string): string[] {
  const normalized = value.trim().toLowerCase();
  const risks: string[] = [];
  if (normalized.includes("*")) {
    risks.push("wildcard hosts");
    if (isPrivateNetworkHostPattern(normalized)) {
      risks.push("private-network wildcards");
    }
  }
  return risks;
}

function permissionPathRisks(value: string): string[] {
  const normalized = sanitizePermissionScalar(value);
  const risks: string[] = [];
  if (normalized === "/" || normalized === "\\" || /^[A-Za-z]:[\\/]*$/.test(normalized)) {
    risks.push("filesystem root");
  }
  return risks;
}

function isPrivateNetworkHostPattern(value: string): boolean {
  const normalized = value.toLowerCase();
  const wildcardStripped = normalized.replace(/^\*\./, "");
  if (
    wildcardStripped === "localhost" ||
    wildcardStripped === "local" ||
    wildcardStripped === "internal" ||
    wildcardStripped === "lan" ||
    wildcardStripped === "home" ||
    wildcardStripped === "corp" ||
    wildcardStripped === "private" ||
    wildcardStripped.endsWith(".local") ||
    wildcardStripped.endsWith(".internal") ||
    wildcardStripped.endsWith(".lan") ||
    wildcardStripped.endsWith(".home") ||
    wildcardStripped.endsWith(".corp") ||
    wildcardStripped.endsWith(".private")
  ) {
    return true;
  }
  if (
    wildcardStripped.startsWith("10.") ||
    wildcardStripped.startsWith("127.") ||
    wildcardStripped.startsWith("192.168.") ||
    wildcardStripped.startsWith("169.254.")
  ) {
    return true;
  }
  return /^172\.(1[6-9]|2\d|3[0-1])\./.test(wildcardStripped);
}

function hasAvailableDecision(requestParams: JsonObject | undefined, decision: string): boolean {
  const available = requestParams?.availableDecisions;
  if (!Array.isArray(available)) {
    return true;
  }
  return available.includes(decision);
}

function findAvailableCommandAmendmentDecision(
  requestParams: JsonObject | undefined,
): JsonValue | undefined {
  const available = requestParams?.availableDecisions;
  if (!Array.isArray(available)) {
    return undefined;
  }
  return available.find(
    (entry): entry is JsonObject =>
      isJsonObject(entry) &&
      (isJsonObject(entry.acceptWithExecpolicyAmendment) ||
        isJsonObject(entry.applyNetworkPolicyAmendment)),
  );
}

function commandRejectionDecision(
  requestParams: JsonObject | undefined,
  preferred: "decline" | "cancel",
): JsonValue {
  const available = requestParams?.availableDecisions;
  if (!Array.isArray(available)) {
    return preferred;
  }
  if (available.includes(preferred)) {
    return preferred;
  }
  const alternate = preferred === "decline" ? "cancel" : "decline";
  if (available.includes(alternate)) {
    return alternate;
  }
  return preferred;
}

function approvalResolutionMessage(outcome: AppServerApprovalOutcome): string {
  if (outcome === "approved-session") {
    return "Codex app-server approval granted for the session.";
  }
  if (outcome === "approved-once") {
    return "Codex app-server approval granted for this turn.";
  }
  if (outcome === "cancelled") {
    return "Codex app-server approval cancelled.";
  }
  if (outcome === "unavailable") {
    return "Codex app-server approval unavailable.";
  }
  return "Codex app-server approval denied.";
}

function approvalScopeForOutcome(outcome: AppServerApprovalOutcome): "turn" | "session" {
  return outcome === "approved-session" ? "session" : "turn";
}

function approvalEventScope(
  method: string,
  outcome: AppServerApprovalOutcome,
): Pick<AgentApprovalEventData, "scope"> {
  return method === "item/permissions/requestApproval"
    ? { scope: approvalScopeForOutcome(outcome) }
    : {};
}

function approvalKindForMethod(method: string): AgentApprovalEventData["kind"] {
  if (method.includes("commandExecution") || method.includes("execCommand")) {
    return "exec";
  }
  if (method.includes("fileChange") || method.includes("Patch") || method.includes("permissions")) {
    return "plugin";
  }
  return "unknown";
}

function isSupportedAppServerApprovalMethod(method: string): boolean {
  return (
    method === "item/commandExecution/requestApproval" ||
    method === "item/fileChange/requestApproval" ||
    method === "item/permissions/requestApproval"
  );
}

function emitApprovalEvent(params: EmbeddedRunAttemptParams, data: AgentApprovalEventData): void {
  params.onAgentEvent?.({ stream: "approval", data: data as unknown as Record<string, unknown> });
}

function readDisplayCommandPreview(
  record: JsonObject | undefined,
): ApprovalPreviewSource | undefined {
  const actionCommand = readCommandActionsPreview(record);
  if (actionCommand) {
    return actionCommand;
  }
  return readCommandPreview(record);
}

function readCommandActionsPreview(
  record: JsonObject | undefined,
): ApprovalPreviewSource | undefined {
  const actions = record?.commandActions;
  if (!Array.isArray(actions)) {
    return undefined;
  }
  let source: ApprovalPreviewSource | undefined;
  for (const action of actions) {
    const command = isJsonObject(action) ? readString(action, "command") : undefined;
    if (!command) {
      continue;
    }
    source = appendPreviewPart(source, command, " && ");
    if (source.clipped) {
      break;
    }
  }
  return source;
}

function readCommandPreview(record: JsonObject | undefined): ApprovalPreviewSource | undefined {
  const command = record?.command;
  if (typeof command === "string") {
    return previewSource(command);
  }
  if (!Array.isArray(command)) {
    return undefined;
  }
  let source: ApprovalPreviewSource | undefined;
  for (const part of command) {
    if (typeof part !== "string") {
      return undefined;
    }
    source = appendPreviewPart(source, part, " ");
    if (source.clipped) {
      break;
    }
  }
  return source;
}

function readStringPreview(
  record: JsonObject | undefined,
  key: string,
): ApprovalPreviewSource | undefined {
  const value = readString(record, key);
  return value === undefined ? undefined : previewSource(value);
}

function readString(record: JsonObject | undefined, key: string): string | undefined {
  const value = record?.[key];
  return typeof value === "string" ? value : undefined;
}

function truncate(value: string, maxLength: number): string {
  return value.length <= maxLength ? value : `${value.slice(0, Math.max(0, maxLength - 3))}...`;
}

function previewSource(value: string): ApprovalPreviewSource {
  return {
    value: value.slice(0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
    clipped: value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH,
  };
}

function appendPreviewPart(
  source: ApprovalPreviewSource | undefined,
  part: string,
  separator: string,
): ApprovalPreviewSource {
  const prefix = source?.value ? `${source.value}${separator}` : "";
  const value = `${prefix}${part}`;
  const clipped = source?.clipped === true || value.length > APPROVAL_PREVIEW_SCAN_MAX_LENGTH;
  return {
    value: value.slice(0, APPROVAL_PREVIEW_SCAN_MAX_LENGTH),
    clipped,
  };
}

function sanitizeApprovalPreview(
  source: ApprovalPreviewSource | undefined,
  maxLength: number,
): SanitizedApprovalPreview {
  if (!source || !source.value) {
    return { omitted: false };
  }
  const rawPreview = source.value.replace(DANGLING_TERMINAL_SEQUENCE_SUFFIX_RE, "");
  const sanitized = sanitizeVisibleScalar(rawPreview);
  if (!sanitized) {
    return { omitted: true };
  }
  return { text: truncate(sanitized, maxLength), omitted: source.clipped };
}

function sanitizeVisibleScalar(value: string): string {
  return value
    .replace(ANSI_OSC_SEQUENCE_RE, "")
    .replace(ANSI_CONTROL_SEQUENCE_RE, "")
    .replace(INVISIBLE_FORMATTING_CONTROL_RE, " ")
    .replace(CONTROL_CHARACTER_RE, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function formatApprovalPreviewSubject(text: string, omitted: boolean): string {
  return omitted ? `${text} ${APPROVAL_PREVIEW_OMITTED}` : text;
}

function joinDescriptionLinesWithinLimit(lines: string[], maxLength: number): string {
  let description = "";
  for (const line of lines) {
    const prefix = description ? "\n" : "";
    const next = `${description}${prefix}${line}`;
    if (next.length <= maxLength) {
      description = next;
      continue;
    }
    const remaining = maxLength - description.length - prefix.length;
    if (remaining < 3) {
      break;
    }
    description += `${prefix}${truncate(line, remaining)}`;
    break;
  }
  return description;
}

function formatErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

¤ Dauer der Verarbeitung: 0.24 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.