📦 Package manager and registries
📌 Description
A package manager in the spirit of apt/homebrew: applications are described by an asc.yaml manifest, published in registries and installed with asc install <package>. Registries can be official or custom, local (file://) or remote (https://), including GitHub repositories.
🎯 Scenarios
asc install nginx— install from the official registry.asc source add https://registry.example.com— connect a custom registry (like an apt source).asc source add https://github.com/user/my-app— an application straight from GitHub (asc.yaml at the root); for a private repo a 404 triggers an offer to configure a token.asc install https://github.com/user/my-app --branch dev— install directly from a repository URL, no registry involved at all (DMN-040): a one-off, for forks and packages that were never published.asc source addabove wires a repo into the registry system permanently; this is for trying one out.- The platform store (🛍️ app-store) is a storefront over the same registries.
🏗️ Technical design
The asc.yaml manifest (draft)
name: my-app
version: 1.2.0
type: docker | native | utility
category: web # a registry topic (databases, ai, bots, game-servers…)
description: "..." # EN in the official registry
settings: ./asc.settings.yaml # optional: the application settings file (see below)
runtime:
image: nginx:1.27 # for docker
stdin: true # docker, optional: keep stdin open (docker run -i) — `asc attach` input reaches the app
tty: true # docker, optional: allocate a pseudo-TTY (docker run -t)
# or install/start/stop commands for native
requirements: { ram: 256M, disk: 1G }
healthcheck: { http: /health }ℹ️ The manifest has no
env:,ports:orvolumes:sections (DMN-027/030): environment variables, published ports and volumes are all declared inasc.settings.yaml— settings with anenv:key and theports/volumessetting types (see below). One source of truth: what the user can configure is exactly what the app gets.
ℹ️ Every
type: dockercontainer gets a raised open-files limit (ulimit nofile, 10240 soft/hard) instead of the Engine's own default of 1024 — several game servers (7 Days to Die's EOS SDK is the documented case) hang or crash during startup on that default with no clearer symptom than the process going silent after its first log line.
🏗️ Local image build: image-build (DMN-050)
A type: docker app can ship its own Dockerfile and have the Engine build the image locally instead of (or beside) pulling a prebuilt one:
runtime:
image-build:
context: . # build context dir, relative to the manifest (default '.')
dockerfile: Dockerfile # relative to the context (default 'Dockerfile')
args: # optional --build-arg values
VERSION: "1.0"
tag: asc-local/my-app # optional; default 'asc-local/<app>:latest'- Only
image→ pull it, as before. - Only
image-build→ build the image from the package Dockerfile at install (and rebuild it on upgrade / a settings-drift recreate; layer caching keeps this cheap). The build context is packed from the package repository and never reaches outside it. - Both
imageandimage-build→ the installer offers a choice: interactivelyasc install <app>prints the two options and asks; non-interactively (or to skip the prompt) pass--image(pull the prebuilt one) or--build(build locally). The chosen source is recorded inmeta.jsonso a later recreate or upgrade uses the same one without asking again.
⚠️ The build runs as the daemon (root). Until per-user container policy lands (DMN-043), building is intended for trusted packages; the base image referenced by
FROMis pulled by the Engine anonymously (private base images for a local build are a later increment).
The build always goes through the Engine's BuildKit backend, not the legacy builder — Dockerfile syntax such as COPY --chmod/--chown needs it, and the legacy builder fails with "the --chmod option requires BuildKit" otherwise. On a terminal, each build step is rendered as a docker build-style progress bar per step (docker pull-style, mirroring layer bars for a pull), regardless of the log level.
📐 JSON schemas of the manifests: asc.schema.json, asc.stack.schema.json and asc.settings.schema.json in the
registryrepository.
Application settings: asc.settings.yaml
Application settings live in a separate file referenced from asc.yaml (settings: ./asc.settings.yaml). It describes the parameters: type, limits, enumerations, defaults — the user fills them in at install time (the platform UI renders a form, the CLI asks questions; silent mode takes the defaults):
settings:
- key: server_name
type: string # string | number | boolean | enum | secret
title: "Server name"
default: "My Server"
required: true
env: SERVER_NAME # exposed to the app as this env variable
- key: max_players
type: number
default: 10
limits: { min: 1, max: 200 } # value limits
- key: difficulty
type: enum # an enumeration
values: [peaceful, easy, normal, hard]
default: normal
- key: game_version
type: enum # presets, but a custom value is also accepted
values: [public, latest_experimental]
default: public
allow_custom: true # any other branch/build id is accepted as-is
- key: rcon_password
type: secret # stored as a secret, masked
required: true
- key: enable_backups
type: boolean
default: true
- key: game_port
type: ports # published container ports (a list)
default: [27015]
limits: { min: 1024, max: 65535 }
env: CS2_PORT # exposed comma-joined; one port — as is
protocol: both # tcp (default) | udp | both
- key: game_data
type: volumes # app volumes (a list, forms below)
default: [/home/steam/cs2-dedicated]- Setting values are saved in
/asc/apps/<id>/config/settings.json(0600 — the file may hold secrets); at install time it is seeded with the defaults, an upgrade adds defaults for new keys without touching the user's choices. - Env pass-through: every setting that declares
env: VAR_NAMElands in the application's environment (secrets included — that is what theirenv:is for). For Docker apps the variables go into the container env at creation. List values (ports,volumes) are exposed comma-joined. type: ports— the published container ports (host port == container port). Declaring the same setting withenv:keeps the app and Docker in sync: the server listens exactly where the port is forwarded.protocolpicks the transport(s) to forward:tcp(default),udp, orboth(the same host==container port on TCP and UDP).type: volumes— the app's volumes; every entry takes one of three forms:/container/path— private app data: the app's data folder (/asc/apps/<id>/data) is mounted at that container path. The folder is created world-writable (0777): images run under arbitrary non-root users and bind mounts keep host ownership; the app directory above it stays restrictive. When the image declares a numericUSER uid:gid, the folder is also chowned to it (DMN-038) — a non-root process may only chown a path it already owns, so an image thatchowns its own data directory on first start (not just writes to it) needs this to avoid EPERM; a named (steam) or bare-uidUSERis left as world-writable only, since its group is only known to the image's own/etc/passwd;/container/path:host— same, but the host side after the colon is used instead ofdata: a plain folder name lands inside the app directory (/asc/apps/<id>/<folder>;repository,configandmeta.jsonare reserved), an absolute path is a host machine path mounted verbatim (a pre-existing directory keeps its ownership and mode);name:/container/path[:ro|:rw]— a Docker named volume, created by the Engine on first use. Named volumes are how several apps share data: one app writes the volume, others mount it:ro(see the cs2 stack in asc-example-apps). Named volumes are not removed with the app.
- Applying changes: a container's configuration is fixed at creation, so on the next
asc app start/asc app restartthe daemon compares the desired state — env, published ports, volumes, quota, start command — with the container's actual one and recreates the container when they differ (or when the container is missing). App data lives in volumes and survives the recreate. If the desired state cannot be computed (say, the registry source is gone), the app still starts as is — availability wins, with a warning in the log. - Changing settings —
asc app settings <id>: an interactive editor in the terminal. It first shows the categories —environments(string/number/boolean/enum/secret settings),ports,volumes,quota,start_command— then the settings of the picked category: pick one by number, enter a value, and it is validated against the definition (type,limits, enumvalues; secrets are masked in the list; ports and volumes take space-separated lists). Also via the platform UI. After a change the application is restarted (asc app restart <id>). allow_custom(type: enum only) — accepts any value outside the declaredvalueslist as free text, instead of rejecting it. Use it for enums that list common presets but should not lock the user out of a value the author didn't anticipate (a game branch/build id, a custom world name). The numbered picker inasc app settingsstill lists the presets; typing anything else is simply accepted.
📏 Resource quota (quota)
The quota: section of asc.settings.yaml limits the resources of one app instance (DMN-021):
quota:
max_cpu: 2 # CPU cores limit (0.5, 2, …)
max_ram: 1G # memory limit: 512M, 2G, … (binary units, like docker -m)
max_disk: 10G # disk usage limit- The values are normalized at install/upgrade time and recorded in
meta.json;asc app info <id>shows them (quota: cpu ≤ 2, ram ≤ 1.0 GiB, …). - Docker apps: enforced at container creation through the Engine API (
NanoCpus,Memory). - native/process apps: recorded in
meta.json; cgroup enforcement is a next increment. max_diskis recorded for every runtime; per-runtime disk enforcement (Docker storage-opt / fs quotas) arrives incrementally.- User overrides (DMN-030): the
quotacategory ofasc app settingsoverrides individual fields on top of the package values ('-'resets a field back); the override lands insettings.jsonand applies through the container recreate on the next restart.
🚀 The start command (start_command)
The application's start command is configured in asc.settings.yaml. The string can interpolate the package's environment variables — ${VAR} syntax:
start_command: "steamcmd +force_install_dir /data +login anonymous +app_update ${STEAM_APP_ID} validate +quit"- The substitution is performed by the daemon at install/upgrade time from the app's env — the setting values with
env:keys, defaults included. An unresolved variable fails the install naming the variable. - Docker apps: the command replaces what the image would run (the entrypoint becomes
/bin/sh -c, so arguments and quoting work as in a shell). - native apps: the command overrides
runtime.startfromasc.yaml. - Interpolation from the application's final environment (org/node/application env levels — 🌱 environments) and a UI preview of the computed command are a next increment; setting values already reach the app as env variables (see the settings section above).
- User override (DMN-030): the
start_commandcategory ofasc app settingsreplaces the package's command for this instance ('-'resets back);${VAR}references resolve from the settings env when the override is applied.
🐳 install/update scripts: native or docker
A package's install and update scripts can run either natively on the host or in docker — controlled by the run_in field in the manifest's scripts: block:
scripts:
install:
run: ./scripts/install.sh
run_in: native # native | docker
update:
run: ./scripts/update.sh
run_in: docker # a one-off container
image: debian:12 # the image for docker execution (optional)native— the script runs on the host as the application's user.docker— the script runs in a one-off container with the/asc/apps/<id>/directory mounted (isolating build dependencies from the host).- By default
run_ininherits the packagetype:docker→ docker,native/utility→ native.
Installation mechanics: cloning the repository
Installing an application = cloning its repository:
asc install <package>→ the daemon clones the package repository into/asc/apps/<id>/repository/.- Application versions = git tags (GitHub tags), read from the repository, not the registry (DMN-047): installing a specific version —
asc install <package>@1.2.0(tag checkout);asc install <package>with no version resolves the repository's newest tag viagit ls-remote(no tags → the default branch HEAD);asc install <package>@(a bare@) lists the repository's tags and branches for an interactive pick (DMN-048), or, non-interactively, returns the available versions as an error. Updating —asc app upgrade <name>(checkout of the new tag). The registry index therefore carries no version field — a package's versions are whatever its repository is tagged with. - License consent (DMN-028, DMN-032): when the cloned package ships a license (
LICENSE.md/LICENSE/LICENSE.txt), the CLI shows where the package comes from (registry source + repository), prints the license text and asks for acceptance — declining aborts the install and leaves nothing behind. The license is looked up in the package's own directory first (a monorepo package may ship its own license), falling back to the repository root. Non-interactive input accepts automatically with a printed notice; API callers receive a structured error with the source, the repository and the license text (the platform UI renders its own consent dialog). A stack asks once per repository. Packages without a license file install without the prompt. - Custom name and multiple instances (DMN-024, DMN-033): in a terminal
asc installasks for an application name — Enter keeps the default, any other input becomes the app's name. Non-interactively, pass--name "My Server". Installing a package that is already installed no longer fails: it becomes a new instance with the next free id (<package>-2,<package>-3, …), which also becomes itscustom_nameunless--nameoverrides it. The name is stored inmeta.json(custom_name), must be unique among the user's apps, survives upgrades, and every command accepts it interchangeably with the id. A suffixed instance records the registry package it came from (meta.package), soasc app upgradekeeps resolving it correctly. - From then on the daemon works with the local copy: reads
asc.yaml/asc.settings.yaml, builds/launches according to the application type. - For Docker applications the container is created through the Engine API; an image missing on the host is pulled automatically from its registry (
runtime.image; a name without a tag meanslatest) — both at install and at upgrade. - Progress bars: on a terminal, the repository clone (
git clone --progress) and a Docker image pull both render as live bars (docker pull/docker-compose pullstyle — one bar per image layer) — on by default, independent ofasc config debug. Non-interactive callers (piped output, the daemon API) get none of this; the same events are always available asdebug-level tracing (asc config debug on).
Requirements at start (DMN-029): the manifest's requirements (ram, disk, cpu) are compared with what the host has free when the app starts. When short, asc app start warns with the exact figures and — in a terminal — asks whether to start anyway at the user's own risk; non-interactive callers get the warning on stderr and proceed. The check is advice, not enforcement: read failures never block the start.
Updating (asc app upgrade <name>[@version], synonym — asc upgrade): the application must be stopped; the new tag is cloned next to the current copy (repository.new), the manifest is validated, and only then are the directories swapped and the runtime recreated (for Docker the container is recreated with the new image). A failure before the swap does not touch the installed application; a failure while recreating the runtime rolls back to the previous version. Without an explicit version, the repository's newest tag is used (DMN-047); a repository with no tags cannot be upgraded without an explicit @version.
Direct install from a git repository (asc install <url> [--branch <name>|--tag <name>], DMN-040): when the spec is a git URL (https://, ssh://, or the scp-like git@host:path) rather than a package name, the daemon skips the registry entirely and clones the repository straight in — the same clone/manifest/provision pipeline as a registry install, minus the resolution step. asc.yaml must be at the repository root (no monorepo path, since there is no registry entry to carry one). --branch/--tag pick the ref to check out; neither flag clones the default branch HEAD. The app id defaults to the repository's own name (bar for both https://github.com/foo/bar.git and git@github.com:foo/bar); --name overrides it, same as a registry install. Private repositories reuse the exact same asc auth credentials below — a URL install is just a registry install with the resolution step removed. asc upgrade does not yet resolve apps installed this way (there is no registry entry to re-resolve against); re-running asc install with the same URL creates a new instance instead of upgrading in place.
Credentials (DMN-045/046): one per-user store holds authorization both for private repositories (git clone) and for private image registries (the Engine pull), told apart by a type field (repo | registry). Entries are keyed per host or prefix (github.com/myorg, ghcr.io/myorg) and stored separately from the source lists, as JSON: /etc/asc/auth.json (root) and ~/.asc/auth.json (user, alongside the rest of the per-user DMN-041 tree), both files 0600. Secrets end up neither in world-readable files nor in the argv of git processes (argv is visible to all users via /proc).
{
"credentials": [
{ "type": "repo", "pattern": "github.com/myorg", "token": "ghp_xxx" },
{ "type": "registry", "pattern": "ghcr.io/myorg", "username": "me", "token": "ghp_yyy" },
{ "type": "repo", "pattern": "github.com/myorg/secret-app", "app": "6f8a…uuid", "key": "/home/me/.ssh/id_ed25519" }
]
}- Types:
repoauthorizes a git clone — a token forhttps://URLs (viaGIT_ASKPASS+ an environment variable of the git process) or an SSH key forgit@/ssh://URLs (GIT_SSH_COMMANDwith-i <key> -o IdentitiesOnly=yes).registryauthorizes an image pull — a token plus username, sent to the Docker Engine as theX-Registry-Authheader (the Engine, not the daemon, then contacts the registry, so no TLS stack is needed daemon-side). A registry entry rejects an SSH key and requires--username. - App binding (
app, DMN-044): a credential may be scoped to a single application by its uuid or id (--app), so a token serves exactly the app that needs it and no other. An app-bound entry is invisible to every other app and beats an equally specific unbound one for its own app; leavingappunset applies the credential to every app whose URL/image matches the pattern. Matching otherwise follows the same rule for both types: the longest prefix at a path boundary wins, and user entries take priority over system ones. - Detection: git always runs with
GIT_TERMINAL_PROMPT=0andBatchMode=yes— cloning a private repository without configured authorization does not hang on a password prompt but fails with a recognizable error (including the "Repository not found" that GitHub returns for private repositories without access). A private image pull surfaces the Engine's own authorization error. - Interactive setup: on detecting a private repository, the CLI in a terminal asks permission to configure authorization right away: for https — token input, for ssh — a pick from the private keys found in
~/.ssh; the choice is saved and the installation retries automatically. A non-interactive call (API, scripts) gets a structured error with anasc auth add ...hint. - Migration: the pre-DMN-045 TOML files (
/etc/asc/git-auth.toml,~/.config/asc/git-auth.toml) are still read when no JSON store exists — their entries have notypeand load asrepo— and are migrated toauth.jsonon the nextasc authwrite, so configured auth keeps working across the upgrade. - CLI:
asc auth add <host|prefix> [--type repo|registry] --token <token> [--username <user>] [--app <uuid|id>]·asc auth add <host> --ssh-key [path](without a path — an interactive key picker) ·asc auth list(types and methods without secrets) ·asc auth remove <host> [--type repo|registry].
Several applications in one repository: asc.stack.yaml
The root rule: a package repository may contain any number of asc.yaml files in subdirectories, but its root must hold exactly one manifest — either asc.yaml (a single application) or asc.stack.yaml (a stack) that ties all the nested asc.yaml files together. Nested manifests without a root one are not indexed.
The asc.stack.yaml stack manifest lists the applications and the paths to their asc.yaml files:
name: my-stack
version: 1.0.0
description: "..."
apps:
- name: web
path: ./web # the directory with asc.yaml
- name: worker
path: ./worker
- name: db
path: ./db
optional: true # an optional stack componentasc install my-stack— install the whole stack;asc install my-stack/web— just one application from it.- Renaming a stack at install (DMN-034):
asc install my-stack --name prodis a prefix applied to every installed app —prod-web,prod-worker,prod-db— since a stack has no entity of its own, only its member apps.asc install my-stack/web --name prod-webnames just the requested app, as for a single-app install. - A stack can declare dependencies between applications (
depends_on— the startup order); components can beoptional. Environment variables live in each app's ownasc.settings.yaml. - Registries and the platform store index both single
asc.yamlfiles andasc.stack.yamlstacks. - Examples — in the asc-example-apps repository.
Stack install mechanics: the repository is cloned once to read asc.stack.yaml, then every selected application installs like a regular app — with its own repository clone, /asc/apps/<id>/ directory and meta.json:
- The app id is the
namefrom the app's ownasc.yaml(in the example above the stack'swebapp may be namedmy-stack-web); the origin is recorded in meta.json aspackage: "my-stack/web"—asc app upgraderesolves the package through it. - Order: dependencies (
depends_on) install first; cycles are rejected at validation.asc install my-stackinstalls every non-optionalcomponent;asc install my-stack/dbinstalls the requested component (even anoptionalone) plus its dependencies. - Repeat installs: a wanted app (the one(s) requested, not a dependency pulled in alongside them) that is already installed becomes a new instance (DMN-033) instead of being skipped; a dependency that is already installed is still reused, not duplicated. Every app installs atomically (a failure removes only its own directory, previously installed components stay).
- License consent is asked once per repository (one repo = one license), not per stack app.
Registries
- Registry format (the
registryrepo) — a hierarchy of JSON files: the root indexregistry.json→ category filescategories/<topic>.json(databases, ai, bots, game-servers, system-utilities, web…) → optional subcategories (children). Packages come in two kinds:app(asc.yaml) andstack(asc.stack.yaml). Validation schemas — inregistry/schema/. Descriptions are in English. - Source tree: the daemon's sourcelist → a tree is built from all registries (following the root index's
index/childrenlinks), then a merged application list (per user); in search results name conflicts are resolved by source priority. - Name conflicts at install: when several sources provide the requested package,
asc installin a terminal lists the candidates (source name + repository) and asks which one to use (pick a number); non-interactive callers get an error with the same list — pin the registry explicitly:asc install <pkg> --source <name>(API: thesourcefield of InstallAppRequest).asc app upgradeprefers the source the app was installed from. - Source types:
file://(a local directory) andhttps://(a registry, GitHub raw). - Fetch resilience (DMN-036): each registry index file (
registry.json, category files) is a separatecurlinvocation with no connection reuse between them — a wholeasc update/asc searchrun is a short burst of several small HTTPS requests to the same host. A stalled connection on any one of them (CDN throttling, transient network hiccup) used to hang the whole command for up to 5 minutes with zero output. Registry fetches now use a short per-request timeout (20s) plus a couple of retries on transient errors, instead of the generous 300s budget reserved for large downloads (asc-updaterrelease assets). asc updateprogress (DMN-037): on a terminal, every index file gets its own progress line,docker pull/git clonestyle — a spinner while the request is in flight, frozen on its byte count on success or the error on failure. Since the registry is fetched one small file at a time, this is what turns a stuck fetch into a visibly stuck spinner instead of a silent wait.- Per-user source lists: two list levels —
- system
/etc/asc/sources.toml— managed by root (sudo asc source add|remove), the sources are visible to all users of the server; - user
~/.config/asc/sources.toml— each user maintains their own list (asc source add|removewithout sudo), which extends the system one. - The effective list = system sources (higher priority) + your own; a user cannot shadow or remove system sources (
asc source listshows the origin of every source). Index caches: for root — indata_dir, for a user — in~/.cache/asc/.
- system
- Install policy (
[policy]in/etc/asc/config.toml, managed by root):user_install = "all"(default — users may install any packages: Docker, native, utilities) oruser_install = "docker"(users may install Docker applications only; native apps and utilities are root-only). Applied atasc install; does not apply to root. - Index cache with a TTL +
asc updatefor a forced refresh. - CLI:
asc install|remove|upgrade <pkg>(orasc install <git-url> [--branch|--tag]),asc search <query>,asc source add|remove|list,asc update.
🔗 Related tasks
DMN-003, DMN-018, DMN-038, DMN-040, DMN-045, DMN-046, DMN-047, DMN-048, REG-001, REG-002, BE-002, BE-003 in ROADMAP.md; GRW-011 in ROADMAP-GROWTH.md.