Repository navigation
A task can now be trusted with what it should not leak, and stopped before what it should not do.
Until now a credential had to sit in the process environment in plain text for get_env to read it, and from there it could reach a JSON event, a final diagnostic or the host's process list. Nothing told destroy apart from build, a flaky step had to be written out again to be retried, and a command left behind by race kept running. The dependency cache was trusted on sight, and a URL in lask.json could be read by git as an option. v0.7.0 reads secrets from Vault without changing the program, asks for a typed confirmation where a project says so, gives tasks retry, polling and timeouts, lets a type parameter carry a bound — and closes every one of those leaks.
Secrets from Vault
- A variable whose whole value is
{scheme://reference}is a secret reference, resolved whenget_env,find_envorget_env_orreads it:AWS_SECRET_ACCESS_KEY="{vault://secret/aws#secret_key}". The program stays the same; only the environment changes.has_envdoes not resolve it, and a malformed reference is an error, never a value. LASK_SECRETSlists the stores a run may use; any other scheme isE-IO-SECRET-PROVIDER. The first isvault(Vault and OpenBao), configured from Vault's own variables:VAULT_ADDR,VAULT_TOKEN, AppRole throughVAULT_ROLE_ID/VAULT_SECRET_ID,~/.vault-token,VAULT_NAMESPACEandVAULT_CACERT. KV version 2 mounts are detected from the server, and?version=nselects a version.- A resolved value is masked like a
!!one. A run logs in once and reads each secret once, acrossasynctoo, so the fields of a dynamic secret come from one lease. Nothing is written to disk and the process environment is never changed. lask secrets list [fn]reports the references in the environment without reaching a store.lask secrets check [fn] [--read]checks configuration, reachability, login and read permission per store before a task runs, reads a value only with--read, revokes any lease it issued, and exits 3 on failure. Neither prints a value or a credential.- Five new external I/O error codes:
E-IO-SECRET-PROVIDER,-REF,-UNREACHABLE,-AUTHand-NOT-FOUND.lask cmd --listnotes an environment that reads a reference, without resolving it.
Typed confirmation
lask.jsontakes aconfirmmap from a public function of the entry module to an entry, so runningdestroyordeploy --env prodbecomes a deliberate act:{"confirm": {"deploy": {"when": {"env": ["prod"]}}, "destroy": {}, "reset_db": {"phrase": "reset #{db}"}}}.whennames the parameter values that require confirmation;phraseis what to type, interpolating parameters.runandevalask before anything is evaluated, and only about the function the CLI calls. At a terminal the phrase is typed; elsewhere--confirmapproves, unlessLASK_CONFIRM=ttysays only a typed confirmation counts. A refusal isE-CLI-NOT-CONFIRMED, exit 4.- An entry belongs to the declaration the name resolves to, so a re-export is covered, and only the root project's file applies.
check,run,evalandenvscheck every entry against the program — the function, the parameters it names, the types of the values — and a mismatch isE-MODULE-CONFIRM-TARGET, located at the key inlask.json. --helpgains a Confirmation section and JSON help aconfirmfield; the editor's hover shows the same, and the language server reports errors inlask.jsononlask.json.
Retry, polling and timeouts
retry(delays, body)andretry_if(delays, when, body)run the body again after a failure, waiting the next delay each time. A strategy is just the array of delays, so its length bounds the attempts; once it runs out, orwhenrefuses, the last failure is re-raised unchanged.until(delays, done, body)polls untildoneaccepts the body's value and returns that value. When the delays run out it fails with code 124,E-RUNTIME-UNTIL-EXHAUSTED, naming the last value.timeout(seconds, body)abandons a body that outlives its limit: its command is stopped, the computations it started withasyncare cancelled, and the call fails with code 124,E-RUNTIME-TIMEOUT. The failure is raised at the call, so atryinside the body does not see it.backoff_fixed,backoff_linearandbackoff_exponentialbuild strategies, andbackoff_jitterrandomizes one:retry(backoff_exponential(1, 2, 3), \() -> build())waits 1, 2 and 4 seconds. Each retry and each missed check writes a line to the execution log.- An abandoned command is actually stopped.
racecancelled the computations that lost, but the command one of them was running kept going, and a container outlived itsdocker runclient. Now the process tree gets SIGTERM and, after 3 seconds, SIGKILL (on Windows,taskkill /T); containers are namedlask-<random>and stopped withdocker stop. The log records such a command askilledin place of its exit line.
Bounded type parameters
- A type parameter takes one bound: a named bound —
comparable,orderable,stringifiable— or a type bound, which admits what conforms to it.largest<T: orderable>(xs: Array<T>): T = last(sort(xs))is now a function a user can write. A type alias makes a bound of a program's own. - Inside the body, a type parameter has what its bound gives:
comparablegives==,stringifiablegives#{...},orderablegivessortand both of the others. A type bound gives what the bound has, exceptAny, which gives nothing. - The built-ins state their conditions as bounds:
sort,sort_by,unique,contains_array,index_of_array,to_stringandmark_secret. They used to check them at the call site only, so a built-in referenced as a function value skipped the check —s: Function<Array<Bool>, Array<Bool>> = sortwas accepted. A use outside a bound is the newE-TYPE-BOUND, at a call, at a reference, and where a bounded alias is applied. - The CLI decodes a type-bounded parameter at its bound and instantiates a named-bounded one from the arguments it was given. Help, function lists and hovers show bounds, for built-ins too.
Async
- A never-awaited
asyncis no longer cut short. At the end of a run,runandevalwait for every handle thatawait,allorracedid not consume, including any those start in turn, and report each as the runtime advisoryW-ASYNC-UNAWAITED, with where it started and how it ended. The result and the exit code of the run are unchanged. lask checkreportsW-ASYNC-UNUSEDfor a handle the text alone shows is never awaited: a discarded statement of typeAsyncHandle<T>orArray<AsyncHandle<T>>, or such a binding that nothing refers to. It is the first advisory the implementation reports:checkprints it and still exits 0, withseverity: "warning"in JSON, and the editor shows it as a warning.awaitre-raises a failure unchanged, as 6.3, 8.6 and 8.10 always said, so a command that fails insideasyncexits the process with its own code andcatch (e)sees itse.code.
REPL
:reload/:rreads the target module again and re-applies the declarations typed at the prompt, in order. Re-applying compiles and does not evaluate, so a reload runs nothing.- A module that is missing or does not compile, or a typed declaration that no longer compiles against it, fails the reload by name and leaves the session as it was. A reload never discards what was typed.
- Input starting with
:is a session command; an unknown one is reported rather than parsed as code.
Hardening
- A secret is masked everywhere lask writes to stderr, not only in the command log. With
--format jsona secret passed to a function appeared in its call event, and one a failed command printed to stderr appeared in the final diagnostic. Events, uncaught failures,W-ASYNC-UNAWAITEDand REPL errors are now masked. A secret that spans lines is also masked line by line. - A
dockerenvironment'senvvalues stay off the command line.--env NAME=valuemade a credential readable by every user of the host in the process list. Each variable is now named as--env NAMEand its value handed to docker through the client's environment. Variables the client reads itself (DOCKER_*,*_PROXY,PATH,HOME,SSH_AUTH_SOCK) are passed as before. - The Vault token follows one standby redirect at most. A 307 or 308 is followed once, as the Vault CLI does; any other redirect, a second one, or one from https to http is
E-IO-SECRET-UNREACHABLErather than a token sent to another host. - A dependency source can no longer be read as an option of git or curl. A git URL such as
--upload-pack=<command>ran that command duringlask deps sync.git,revandurlmay not start with-or contain whitespace, aurlmust behttps://,http://orfile://, and both tools take the source where it cannot be an option. A lock entry's hash andrevmust have their exact shapes, so neither can name a path outside the cache. - The dependency cache is verified where it is used. An entry used to be trusted from the moment
deps syncput it there, so anything that could write to a sharedLASK_CACHE_DIRcould replace a dependency's code. The loader now checks each entry against the lock's hash once per resolution, anddeps syncrefetches one that does not verify. - A dependency may not contain a symbolic link, and a local import inside a dependency must stay inside its tree. A link made a dependency's content depend on the machine reading it, and let an import reach any file.
Dependencies and the lock
- A git dependency's
revis the commit its content came from, recorded by whichever ofdeps addanddeps syncfetched it, and a tag that has moved since isE-MODULE-REV-MOVEDbefore anything is fetched.deps addused to pin no commit, anddeps syncresolved references only after fetching, so a moved tag surfaced as a hash mismatch or was pinned with the old content's hash. For an annotated tag,revheld the tag object; such a lock is re-pinned to the commit. deps addno longer rebuilds the lock from scratch. It verifies the other dependencies against the lock, keeps the pinned images, and writes nothing unless every entry resolves.- A transitive dependency is locked under its parent's path,
parent>child, the key the loader looks it up by. It was recorded under its bare name, so a project with a transitive dependency failed withE-MODULE-LOCK-STALEright afterdeps addand could never run. A dependency'slask.jsonthat cannot be read is now a failure of that dependency rather than taken to declare none.
Breaking changes
- Unknown keys in
lask.jsonare errors rather than ignored.dependenciesis now optional. - A lock written by v0.6.0 for a project with a transitive dependency is out of date under
--frozen. Onelask deps syncrewrites it; such a lock never resolved before. - A dependency containing a symbolic link, or a local import that leaves its tree, is refused, as is a dependency source that starts with
-, contains whitespace, or uses a scheme other thanhttps,httporfile. - A built-in referenced as a function value is checked against its bound, so a reference like
s = sortat a type that is notorderableis nowE-TYPE-BOUND. - Every type alias is checked, used or not. An alias with duplicate fields or
Array<Void>that nothing referred to used to pass, and in a library the error surfaced only in a consumer. E-RUNTIME-AWAIT-FAILEDis removed.awaitre-raises the awaited failure unchanged, andraceon an empty array isE-RUNTIME-VALUE.E-NAME-AMBIGUOUS, which nothing ever reported, is removed too.
Lask is pre-1.0 and every feature is experimental until 1.0, so breaking changes remain possible; see compatibility.md.
Fixes and internals
package.yamlis the only package description.lask.cabalis generated from it and no longer committed: with an older stack, a committed cabal file from a newer hpack silently took precedence, and the two had already drifted.- Every error code is raised end to end with its exit code, every example project is checked from a fresh copy, the Lask examples in the spec are checked (three stale ones fixed), and CI records coverage per module. The build is warning-free again.
- The obsolete Terraform state lock file in the webapp example is removed and ignored.
Documentation
- The README opens with a 20-second recording — a typo flagged and fixed in the editor,
lask run releaserunning each step in its own container, and the same run in GitHub Actions, Jenkins, GitLab CI and CircleCI — linking the full one-minute tour, which adds the REPL and secrets read from Vault. - "Why Lask" gains Secure — masked secrets, Vault references, hash-pinned imports, container lockdown and typed confirmation — and folds Discoverable into Runnable and Reusable into Programmable. The README shows an excerpt of the webapp example, compares Lask with Dagger alone now that Earthly is no longer developed, and points questions to Discussions and bugs to Issues.
- Spec 9.8 (secret references) and 11.10 (
lask secrets) are new, and 11.9 documents the REPL session; the project file chapter, the type system and the built-in reference follow every change above, as do the Quick Reference and the language tour.
Full Changelog: v0.6.0...v0.7.0