docs lemc-verbs.md

LEMC verbs

A recipe writes ordinary lines to stdout. Lines that begin with a recognized LEMC verb become structured output events before the server fans them out to the browser and LEMCSSH.

Recipes stay language-agnostic: if a program can print a line, it can emit a LEMC verb.

Wire format

verb.name;payload

Print one verb per line on stdout. The first semicolon separates the verb name from its payload. The server classifies each line before cache, log, fanout, or render. Recognized browser verbs become structured job events for the browser and LEMCSSH. Unrelated process stdout and stderr stay durable diagnostics only.

  • Spell verbs in lowercase with the trailing name before the semicolon (for example lemc.output;).
  • There is no fallback from an unrecognized or misspelled line to lemc.output. Use lemc.output;… when the caller should read the text.
  • lemc.env is private next-step handoff: no client fanout, no render, no durable secret store.
  • Never put secret values in any verb payload, ordinary log line, or artifact.

Verb reference

Major families for recipe authors. Use HTML, CSS, and JavaScript only for reviewed, task-scoped UI. Use plain text for human-readable progress. Use lemc.env only for non-secret handoff inside one job.

VerbEffectTypical payload
lemc.html.appendAppend HTML to the task's rendered output target.<p>ready</p>
lemc.html.truncReplace the current rendered HTML before applying the payload.<h2>new view</h2>
lemc.html.bufferBuffer HTML for later flush on the same task stream.partial markup chunk
lemc.css.appendAppend CSS to the task-scoped style target..ready { color: green; }
lemc.css.truncReplace current task CSS with the payload.#target { display: grid; }
lemc.css.bufferBuffer CSS for later flush on the same task stream.partial style chunk
lemc.js.execExecute the supplied task JavaScript; alias for truncate-and-execute behavior.console.log("ready")
lemc.js.truncReplace current task JavaScript and execute it.document.querySelector(...)
lemc.outputAppend escaped plain text to the visible output stream.readiness check passed
lemc.envCarry a non-secret key/value into later steps of this job only.REPORT_ID=42
lemc.errSignal failure; do not put secrets or large dumps in the body.release gate rejected

Step-progress verbs such as lemc.steps.append and schedule variants (lemc.steps.now.*, lemc.steps.in.*, lemc.steps.every.*) update the task step surface the same way HTML verbs update the main render target. Prefer small, reviewed fragments over unbounded HTML dumps.

Minimal shell example

#!/bin/sh
set -eu

printf '%s\n' 'lemc.html.trunc;<section class="report"><h2>Ready</h2></section>'
printf '%s\n' 'lemc.css.append;.report { padding: 1rem; color: #315b35; }'
printf '%s\n' 'lemc.output;release readiness checks completed'
printf '%s\n' 'lemc.env;READINESS_STATUS=ready'

The HTML and CSS appear through the shared event path. The plain output remains readable in both clients, and READINESS_STATUS becomes available to the next step.

The recipe environment contract

A LEMC task runs in a fresh runtime, but its authorized state does not have to start over. Before the first container starts, LEMC hydrates the scoped workspace and assembles the declared environment. Every step sees the appropriate mounted files. Before teardown, LEMC synchronizes accepted state, artifacts, logs, render cache, and terminal proof so a later task can continue the work.

The variables below identify the admitted job and its output targets. Read them; recipe code must not invent identity or authority.

Standard variableUse it for
LEMC_STEP_IDIdentify the current step number.
LEMC_SCOPEDistinguish individual and shared work.
LEMC_USER_ID, LEMC_USERNAMEName the initiating user context.
LEMC_UUID, LEMC_RECIPE_NAME, LEMC_PAGE_IDIdentify the cookbook or app, recipe, and page.
LEMC_HTML_ID, LEMC_CSS_ID, LEMC_JS_IDTarget this task's rendered output without colliding with another job.
LEMC_HTTP_DOWNLOAD_BASE_URLBuild an authorized link to a file written under /lemc/public.

Form fields become environment variables

This is the form-to-environment path people sometimes call “templating.” There is no separate template engine. A recipe form field becomes one process environment variable inside the job. Recipe code reads ordinary shell or language env APIs.

Supported field types: text, password, textarea, select, and radio. Prefer variable for the env name and description for the human label. name remains a compatibility alias when variable is absent. When both appear, variable wins.

YAML fieldRequiredRole
variableYes*Environment variable base name (preferred).
nameYes*Compatibility alias for the same base name.
descriptionNoHuman-readable label in the UI.
typeYestext, password, textarea, select, or radio.
optionsNoFor select/radio: separate label and value pairs.
defaultsNoPlaceholder or default entries; in the compatibility form each item is both label and value.

*Either variable or name is required.

Create an author-defined value

Define a cookbook form field with variable. LEMC turns its name into uppercase environment form, replacing spaces and hyphens with underscores. The submitted value—not its display label—enters the recipe container.

form:
  - variable: release-channel
    description: Release channel
    type: select
    options:
      - label: Stable
        value: stable
      - label: Preview
        value: preview
  # Compatibility form: name + defaults (label and value are the same string).
  - name: region
    type: radio
    defaults:
      - us-east-1
      - us-west-2
# Selected values are uppercase env vars (value, not label).
# variable: release-channel → RELEASE_CHANNEL=stable|preview
printf 'lemc.output;channel=%s\n' "$RELEASE_CHANNEL"
# name: region → REGION=us-east-1|us-west-2
printf 'lemc.output;region=%s\n' "$REGION"

# Create a non-secret value for the next step only.
printf '%s\n' 'lemc.env;REPORT_NAME=report.txt'

Conversion rules:

  • variable: deployment_env with value prodDEPLOYMENT_ENV=prod.
  • variable: log-level with value debugLOG_LEVEL=debug.
  • My_Param / my-mixed-ParamMY_PARAM / MY_MIXED_PARAM.
  • For options, the container receives the option value, never the emoji or marketing label.

lemc.env is a private next-step handoff. LEMC does not cache, log, render, or fan it out to browser and LEMCSSH gateway clients. Put secrets in named server-side bindings and use visible output or lifecycle verbs only for values callers should observe.

Non-secret environment precedence

Non-secret sources combine in this order. Later sources override earlier non-secret sources. Secrets never lose to a public, form, system, or step key with the same name—collisions fail closed.

  1. Cookbook public environment (cookbook.environment.public)
  2. Recipe public environment (recipe.environment.public)
  3. Form input (uppercase variables from the form above)
  4. System variables (the LEMC_* table)
  5. Step environment (environment preferred; env is a compatibility alias)

Read declared secrets as environment variables

Cookbook and recipe YAML list secret names under environment.secrets. The original cookbook author binds each value in their write-only User Secrets catalog. At job time LEMC injects each accepted value into the recipe process environment under that exact name. The teammate or agent who runs the app does not get a personal copy of the key; they inherit the author’s capability through app ACLs.

cookbook:
  environment:
    secrets:
      - GCP_SERVICE_ACCOUNT_JSON
      # Namespace when one author holds more than one key of the same kind.
      - TEAM1_GCP_SERVICE_ACCOUNT_JSON
      - TEAM2_GCP_SERVICE_ACCOUNT_JSON
# Exact declared names become process environment variables.
test -n "${GCP_SERVICE_ACCOUNT_JSON:-}"
test -n "${TEAM1_GCP_SERVICE_ACCOUNT_JSON:-}"
# Never print, log, or write secret values to /lemc/public or verbs.
printf 'lemc.output;status=authenticated\n'

Names match [A-Z_][A-Z0-9_]*. The LEMC_ prefix is reserved for platform variables. Secret names must not collide with form variables, public env entries, or step env keys. Prefer purpose prefixes (TEAM1_, TEAM2_, project or environment codes) so two GCP keys never share one bland name in a single author catalog. See Secrets become environment variables.

Give each kind of state the right lifetime

The recipe image is read-only and /tmp is ephemeral. Durable task state belongs under a scoped /lemc workspace, where LEMC can validate, synchronize, and hydrate it independently of the guest that produced it.

BoundaryLifetime and purpose
/lemc/privatePrivate durable workspace for this user and context. Keep working state such as terraform.tfstate here so a later authorized task can hydrate and use it.
/lemc/publicDurable downloadable artifacts. Link an exact task result with LEMC_HTTP_DOWNLOAD_BASE_URL.
/lemc/globalCookbook UUID or locker-scoped data used across the cookbook context.
/lemc/sharedTeam workspace mounted only when the recipe runs in shared scope.
lemc.envOne small, non-secret value carried only to the next step in the current job.
Image root and /tmpRead-only recipe code plus ephemeral scratch. Do not put state here when another step or task will need it.

Example: preserve Terraform state after the runner is gone

An apply task writes Terraform's state into the private workspace. A second step can turn that state into a downloadable summary. A destroy task hours later enters a new guest, receives the same authorized private workspace, and acts on the state created by apply.

# Apply task, step 1: update durable private state.
terraform -chdir=/lemc/private apply -auto-approve
test -s /lemc/private/terraform.tfstate
printf '%s\n' 'lemc.env;REPORT_NAME=apply-summary.txt'

# Apply task, step 2: REPORT_NAME arrived through lemc.env.
terraform -chdir=/lemc/private show -no-color > "/lemc/public/$REPORT_NAME"
report_url="${LEMC_HTTP_DOWNLOAD_BASE_URL}${REPORT_NAME}"
printf 'lemc.html.append;<a href="%s">Download apply summary</a>\n' "$report_url"

# Later destroy task: LEMC hydrated the private workspace first.
test -s /lemc/private/terraform.tfstate
terraform -chdir=/lemc/private destroy -auto-approve

The compute boundary is disposable; the authorized workspace is not. LEMC verifies durable writes before local scratch deletion, so teardown does not discard the state required for follow-up work.

Scoped targets

LEMC injects task-specific element IDs so recipe CSS and JavaScript can target their own output.

LEMC_HTML_ID
The current job's HTML output element ID.
LEMC_CSS_ID
The current job's style element ID.
LEMC_JS_ID
The current job's script element ID.
selector="#${LEMC_HTML_ID} .result"
printf 'lemc.css.append;%s { font-weight: 700; }\n' "$selector"

HTML, CSS, and JavaScript output is executable content. Publish recipes only through the same review, image authorization, permissions, and isolation policy used for other code.

Link a durable artifact

Write downloadable output under /lemc/public and build links from the injected immutable task-scoped base URL.

report_path="/lemc/public/readiness.txt"
printf '%s\n' 'release is ready' > "$report_path"

report_url="${LEMC_HTTP_DOWNLOAD_BASE_URL}readiness.txt"
printf 'lemc.html.append;<p><a href="%s">Download readiness report</a></p>\n' \
  "$report_url"

The terminal job path syncs the artifact to durable storage before scratch cleanup. Browser and LEMCSSH consumers should use LEMC's authorized artifact routes rather than a runner-local path.

Errors and diagnostics

  • Use lemc.err for a concise failure the caller can act on.
  • Use ordinary stdout/stderr for bounded technical diagnostics.
  • Never print credentials, bearer tokens, registry secrets, or private payloads.
  • Do not flood the visible stream with successful pull/provisioning chatter.
  • Write durable artifacts when the result is larger than a useful live message.

Multi-step values

# Step 1
printf '%s\n' 'lemc.env;REPORT_NAME=readiness.txt'

# Step 2 receives REPORT_NAME in its environment.
printf 'lemc.output;publishing %s\n' "$REPORT_NAME"

Treat lemc.env as explicit handoff inside one job, not as durable workspace state or a secret store. Put files a later job needs under the correct /lemc workspace. Sensitive inputs belong in the author’s named secret catalog, arrive as environment variables for the job only, and must remain redacted from events and logs.