Skip to content

v0.7.0

Latest

Choose a tag to compare

@Ikepeeee Ikepeeee released this 04 Oct 14:02
9757331

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 when get_env, find_env or get_env_or reads it: AWS_SECRET_ACCESS_KEY="{vault://secret/aws#secret_key}". The program stays the same; only the environment changes. has_env does not resolve it, and a malformed reference is an error, never a value.
  • LASK_SECRETS lists the stores a run may use; any other scheme is E-IO-SECRET-PROVIDER. The first is vault (Vault and OpenBao), configured from Vault's own variables: VAULT_ADDR, VAULT_TOKEN, AppRole through VAULT_ROLE_ID / VAULT_SECRET_ID, ~/.vault-token, VAULT_NAMESPACE and VAULT_CACERT. KV version 2 mounts are detected from the server, and ?version=n selects a version.
  • A resolved value is masked like a !! one. A run logs in once and reads each secret once, across async too, 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, -AUTH and -NOT-FOUND. lask cmd --list notes an environment that reads a reference, without resolving it.

Typed confirmation

  • lask.json takes a confirm map from a public function of the entry module to an entry, so running destroy or deploy --env prod becomes a deliberate act: {"confirm": {"deploy": {"when": {"env": ["prod"]}}, "destroy": {}, "reset_db": {"phrase": "reset #{db}"}}}. when names the parameter values that require confirmation; phrase is what to type, interpolating parameters.
  • run and eval ask before anything is evaluated, and only about the function the CLI calls. At a terminal the phrase is typed; elsewhere --confirm approves, unless LASK_CONFIRM=tty says only a typed confirmation counts. A refusal is E-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, eval and envs check every entry against the program — the function, the parameters it names, the types of the values — and a mismatch is E-MODULE-CONFIRM-TARGET, located at the key in lask.json.
  • --help gains a Confirmation section and JSON help a confirm field; the editor's hover shows the same, and the language server reports errors in lask.json on lask.json.

Retry, polling and timeouts

  • retry(delays, body) and retry_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, or when refuses, the last failure is re-raised unchanged.
  • until(delays, done, body) polls until done accepts 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 with async are cancelled, and the call fails with code 124, E-RUNTIME-TIMEOUT. The failure is raised at the call, so a try inside the body does not see it.
  • backoff_fixed, backoff_linear and backoff_exponential build strategies, and backoff_jitter randomizes 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. race cancelled the computations that lost, but the command one of them was running kept going, and a container outlived its docker run client. Now the process tree gets SIGTERM and, after 3 seconds, SIGKILL (on Windows, taskkill /T); containers are named lask-<random> and stopped with docker stop. The log records such a command as killed in 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: comparable gives ==, stringifiable gives #{...}, orderable gives sort and both of the others. A type bound gives what the bound has, except Any, which gives nothing.
  • The built-ins state their conditions as bounds: sort, sort_by, unique, contains_array, index_of_array, to_string and mark_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>> = sort was accepted. A use outside a bound is the new E-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 async is no longer cut short. At the end of a run, run and eval wait for every handle that await, all or race did not consume, including any those start in turn, and report each as the runtime advisory W-ASYNC-UNAWAITED, with where it started and how it ended. The result and the exit code of the run are unchanged.
  • lask check reports W-ASYNC-UNUSED for a handle the text alone shows is never awaited: a discarded statement of type AsyncHandle<T> or Array<AsyncHandle<T>>, or such a binding that nothing refers to. It is the first advisory the implementation reports: check prints it and still exits 0, with severity: "warning" in JSON, and the editor shows it as a warning.
  • await re-raises a failure unchanged, as 6.3, 8.6 and 8.10 always said, so a command that fails inside async exits the process with its own code and catch (e) sees its e.code.

REPL

  • :reload / :r reads 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 json a 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-UNAWAITED and REPL errors are now masked. A secret that spans lines is also masked line by line.
  • A docker environment's env values stay off the command line. --env NAME=value made a credential readable by every user of the host in the process list. Each variable is now named as --env NAME and 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-UNREACHABLE rather 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 during lask deps sync. git, rev and url may not start with - or contain whitespace, a url must be https://, http:// or file://, and both tools take the source where it cannot be an option. A lock entry's hash and rev must 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 sync put it there, so anything that could write to a shared LASK_CACHE_DIR could replace a dependency's code. The loader now checks each entry against the lock's hash once per resolution, and deps sync refetches 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 rev is the commit its content came from, recorded by whichever of deps add and deps sync fetched it, and a tag that has moved since is E-MODULE-REV-MOVED before anything is fetched. deps add used to pin no commit, and deps sync resolved 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, rev held the tag object; such a lock is re-pinned to the commit.
  • deps add no 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 with E-MODULE-LOCK-STALE right after deps add and could never run. A dependency's lask.json that cannot be read is now a failure of that dependency rather than taken to declare none.

Breaking changes

  • Unknown keys in lask.json are errors rather than ignored. dependencies is now optional.
  • A lock written by v0.6.0 for a project with a transitive dependency is out of date under --frozen. One lask deps sync rewrites 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 than https, http or file.
  • A built-in referenced as a function value is checked against its bound, so a reference like s = sort at a type that is not orderable is now E-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-FAILED is removed. await re-raises the awaited failure unchanged, and race on an empty array is E-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.yaml is the only package description. lask.cabal is 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 release running 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