feat(registry): push/pull + run agent/bundle from the registry (typed-artifact schema v2, mounts, filesets)#61
feat(registry): push/pull + run agent/bundle from the registry (typed-artifact schema v2, mounts, filesets)#61msa0311 wants to merge 25 commits into
Conversation
Phase 1 of the lns-registry client integration: authenticate to any OCI registry and push/pull a policy as a typed OCI artifact. - `lns login`/`logout`/`auth list`: store registry credentials in a new per-machine 0600 JSON store (`~/.lns-registry-auth.json`) behind a `RegistryCredentialStore` seam, kept separate from workload credentials so registry tokens never reach a sandboxed workload. Tokens are only accepted via `--password-stdin` and are never printed. - `lns policy push <file> <ref>` / `pull <ref>`: encode a `Policy` as the registry's typed config blob (mediaType `application/vnd.lens.policy.config.v1+json`, manifest artifactType `application/vnd.lens.policy.v1+json`), schema-valid by construction. The CLI stays a thin IPC client; lns-service owns the oci-client push/pull and resolves `RegistryAuth` from the stored credential by registry host. - New IPC messages PolicyPush/PolicyPull; Registry auth threaded per-call. Tests: Layer 2 behaviours for auth and a policy push→pull round-trip; Layer 3 units for artifact encode/decode (incl. JSON-schema validity), the credential store, the service-side orchestration, and the IPC codec. Full gate green.
…IN_HTTP override) oci-client defaults to HTTPS, so a local/dev registry served over plain HTTP (e.g. a port-forwarded in-cluster registry on localhost:5000) is unreachable. Mirror Docker/containerd: a loopback registry (localhost / 127.0.0.1 / ::1) is spoken to over plain HTTP automatically, with no flag or env var. For the rarer non-loopback HTTP dev registry, `LNS_REGISTRY_PLAIN_HTTP` is a comma-separated host[:port] allowlist. The protocol is derived from the target reference's registry host in the daemon (which owns the OCI client); `registry_protocol`/`is_loopback_registry` are pure host-tested helpers, the ref parse + env read live in the IGNORES'd wiring.
add2230 to
6442702
Compare
|
Login part was already implemented in #66 |
…r all families Replace the policy-only `lns policy push`/`pull` with a single Docker-like verb pair that works for every typed artifact family (agent, policy, tool, workflow, sandbox, knowledge, integration, bundle) and, on pull, transparently caches container images. - `lns push <file> <ref> [--family X]`: uploads the file (YAML or JSON) as the family's config blob; family is inferred from the reference's `…/<segment>/…` path or `--family`. The registry validates the blob against its schema. - `lns pull <ref> [-o file]`: the registry's config mediaType decides — a typed artifact is written out (file or stdout); an OCI image is pulled into the cache. - lns-policy `artifact` module is now the family taxonomy (slug / media types / path-segment / inference) + a YAML-or-JSON → JSON `to_config_blob`; the policy-specific encoder is gone. - Generic IPC `PushArtifact`/`Pull` (→ `Pushed`/`PulledArtifact`/`PulledImage`) replaces `PolicyPush`/`PolicyPull`; the service `artifact` module generalizes the push/pull orchestration; image-vs-artifact is decided from the manifest. `lns policy allow/deny/list/remove` (local editing) are unchanged. Image push and threading registry auth into `lns run` image pull are the next stages. Full gate green; Layer 2 `registry.feature` round-trips policy and agent files.
… + loopback HTTP Image pulls (lns pull <image>, lns run <image>) now build the OCI client for the target reference: loopback / LNS_REGISTRY_PLAIN_HTTP protocol selection and the stored credential for that registry (anonymous if none). Previously image pull was hardcoded to anonymous HTTPS, so private and localhost registries were unreachable. - artifact::resolve_auth(reference): best-effort stored-credential lookup for a reference's registry, anonymous on any parse/load failure. - RealRegistry::for_reference(reference): protocol + auth wired from the reference; used by image::pull. RealRegistry::new() retired.
…s's cache
`lns push <image-ref> <target-ref>` (a source that isn't a local file) now
re-pushes an image lns has already pulled: the daemon reconstructs it from the
manifest + layer caches and uploads it to the target with the target's stored
credential and loopback/HTTP protocol.
- IPC PushImage{source_reference,target_reference}; service artifact::push_image
orchestration; RealRegistry::push_image_from_cache reads CachedManifest + layer
blobs and oci-client `push`es them.
- CLI push() routes a non-file source to push_image. Layer 2 covers it.
`lns push <cached-image>` only worked for digest-pinned pulls because it reconstructed the push from the manifest cache, which intentionally skips mutable tags. Tag-pulled images had no manifest-cache entry, so push failed with "not in the local image cache" even though the image was cached. Persist the raw manifest + config blob on PulledImage and in ImageRecord (both Option + serde default for back-compat; an old record reads as None and asks for a re-pull), then reconstruct the push from the record plus the layer cache instead of the manifest cache.
`lns run <ref>` now accepts a typed agent or bundle artifact, not just an OCI image. An agent reference resolves to its image + command; a bundle (kind: AgentSystem) resolves its single agent component and materializes the bundle's egress policy into a run-scoped ephemeral file, leaving the project's lns-policy.yaml untouched. Required credentials that are not yet provisioned locally surface a non-fatal connect warning (the sandbox still asks at the boundary). A plain image or a non-runnable family (policy, tool, …) is unchanged or refused with a clear message. Artifact deserializers (AgentArtifact, BundleArtifact) live in lns-policy, the registry-contract crate; the CLI-side resolver is a pure, injectable unit driven through the RegistryClient port so it is fully host-tested.
…ifact The agent artifact now also encodes resources (cpus/memoryMib), sandbox user, published ports, and volumes; the resolver maps them onto RunArgs (loopback TCP for ports, MiB memory), with explicit CLI flags still winning. This lets `lns run <bundle-ref>` reproduce a full run — dashboard port, persistent volume, and resource sizing — instead of just image + command + credentials.
CPU/memory now live solely on the sandbox artifact (the runtime envelope), not the agent. The bundle resolver pulls the sandbox component and applies its `resources` (cpu/memory, number or string per the registry schema); the agent artifact no longer carries resources. A bare agent-ref run, or a bundle with no sandbox component, falls back to CLI flags or defaults. Explicit --cpus/--mem still win. Adds a SandboxArtifact deserializer to lns-policy and Quantity coercion (reusing cli::parse_mem_arg for string memory). isolation/capabilities/baseImage are parsed but not yet applied (no RunArgs knob / digest-pinned image pending the pull_inner fix).
…ent isolation
Adopt the lns-registry two-class schema in the artifact contract: add the Model
and Fileset families (now 10), the optional envelope `mount {path, readOnly?}`
(application-layer only), and the ModelArtifact/FilesetArtifact/MountedArtifact
deserializers plus Family::{default_mount_path, is_application_layer}. References
become ArtifactRef {ref, digest?}; bundle components gain a single `model`. The
agent artifact drops `isolation` (now sandbox-only); the sandbox gains tolerant
supervisorVersion/capabilities/baseImage. Consume/mount side follows.
Add ArtifactMount {reference, path, read_only} to RunImageArgs/RunConfig. The
CLI resolver turns a bundle's application-layer components into mounts: it pulls
each component's config blob to read its family + metadata.name + envelope mount,
computes the guest path (canonical default or explicit override; fileset requires
an explicit path), rejects runtime-layer refs and images, and emits an
ArtifactMount. The bundle resolver wires the `model` component through this; the
service-side materialization at boot follows.
artifact::materialize_mounts resolves each ArtifactMount and turns it into a RuntimeFileSpec: a model artifact's config blob is written at its mount path (default /etc/agent/model). runtime_layer::for_run takes the extra specs and folds them into the composefs runtime layer, so the files land read-only in the guest at boot. Duplicate mount paths are rejected; layer-content families (tool/knowledge/fileset) are deferred to the next stage with a clear error.
…t root ArtifactRegistry gains pull_artifact_layers (real impl pulls the artifact manifest and each layer blob via oci_client). materialize_mounts now handles the layer-content families: tool/knowledge/fileset layers are gz-sniffed, walked with a tar-slip guard (safe_rel_path rejects ../, absolute, and non-UTF8 components), and emitted as RuntimeFileSpecs rooted at the mount path. Empty layer sets warn and no-op; runtime-layer families are rejected.
resolve_bundle_ref now resolves the model, tool, and knowledge components into
artifact mounts (canonical paths /etc/agent/{model,tools/<name>,knowledge/<name>}
unless the artifact overrides via its envelope mount). Drops the interim
note_skipped_components — these components are applied now, not skipped.
Round-trip against a PR#5 registry surfaced that every family nests its
family-specific fields under spec (the registry rejected a top-level-components
bundle with MANIFEST_INVALID "spec is a required property"). Move BundleArtifact
to spec.components and SandboxArtifact to spec.{isolation,resources,…}; the
resolver reads them accordingly. materialize_policy now unwraps the policy
artifact envelope's spec so the supervisor still gets a flat {network,
integrations} file (falls back to a flat blob for back-compat).
msa0311
left a comment
There was a problem hiding this comment.
Reviewed against the lns-registry PR #5 contract (registry-side author here). Overall: this is a faithful, careful adoption of the v2 schema — I'm happy with it. The schema mapping is exact and the mount/untar rail is genuinely well-built. A few points below; none are blockers.
Strong points
- Schema adoption is exact (
lns-policy/src/artifact.rs): all 10 families, correct runtime/application split (is_application_layer),/etc/agent/*defaults matching the registry,ArtifactRef {ref,digest?},model+fileset, bundlecomponents.model, agent droppedisolation. The genericMountedArtifactreader (just{metadata, mount?}) is a nice way to mount any application-layer artifact without modelling every spec. - The untar rail is secure, with defense in depth. Two independent gates —
safe_rel_path(lns-service/src/artifact.rs) rejects../absolute/non-UTF8 tar entries, andnormalize_guest_path(runtime_layer/mod.rs) rejects..again at build time. gz magic-byte sniff, duplicate-mount-path detection, family-gated dispatch (non-mountable family → hard error). This is the part I scrutinized hardest and it's right. - Tests are thorough (behaviour features + unit coverage of deserializers, blob conversion, gzip layer expansion, the traversal guard).
Points worth addressing
1. Model mount format — the one real contract decision (let's align).
For model the code mounts the full envelope ({apiVersion,kind,metadata,spec}) at /etc/agent/model, whereas tool/knowledge/fileset mount raw layer files and policy is unwrapped to spec for the supervisor. What a compliant agent reads at /etc/agent/model is the standard, so I've now pinned it in the registry's docs/ARTIFACTS.md ("Mount file format — the consume contract"):
modelmounts itsspec({provider, model, parameters?}), not the full envelope — so the agent reads config directly without peeling a k8s-style wrapper (mirrors the policy unwrap; identity is already implied by the mount path).- tool/knowledge/fileset mount layer files verbatim (no envelope).
PRISM hasn't implemented the layout yet, so we get to define it — proposing spec-only. This would be a ~one-line change here (unwrap spec like materialize_policy does). Happy to discuss if you/PRISM prefer the full envelope.
2. mount.path is now constrained registry-side. I added a schema rule on PR #5: mount.path must be absolute + traversal-free (leading /, no ..). So a bad fileset path now fails at push with a clear message rather than late in normalize_guest_path mid-boot. No change needed here — just a heads-up that the surface tightened.
3. read_only — parsed and threaded, but is it enforced? It reaches RunImageArgs.artifact_mounts{read_only}, but I didn't see it affect the composefs injection (everything goes in at mode & 0o7777). Worth confirming a readOnly: true mount is actually non-writable in the guest, or documenting it as advisory for now.
4. Symlink targets in layers are unsanitized (expand_layer_to_specs) — but they only ever resolve inside the guest's own FS, so not a host escape. Fine to leave; a one-line comment noting it's intentional would help the next reader.
The integrations ArtifactRef-vs-string mismatch you flagged is real and correctly deferred — that's the next thing to land before integrations work end-to-end.
…ring) lns push --content <dir|file> packs the local content tree into a single gzip tar layer and forwards it alongside the config blob. The layer rail threads through RegistryClient::push_artifact, the PushArtifact IPC frame, and the service-side ArtifactRegistry down to the OCI manifest, so a fileset's content lands in the registry as real image layers.
…r artifact Mounts a fileset/model/tool/knowledge artifact into the guest at the path the artifact declares (or an explicit override after :/). Explicit mounts ride alongside any bundle-resolved ones. This is the interim attach for filesets until the registry bundle schema grows a filesets component key.
The registry stores policy.spec.integrations as ArtifactRef maps ({ref}),
but the supervisor reads a flat policy with integrations as bare id strings.
materialize_policy now rewrites each {ref} to the integration id (last path
segment, tag stripped) when unwrapping the envelope spec, so a bundle policy
that references integrations loads instead of failing with a type mismatch.
|
Follow-up on the deferred "bundle I've closed the registry gap on lns-registry PR #5: the bundle schema now has So
Net: |
…pulls pull_inner re-hashed a re-serialized copy of the manifest and compared it to the requested digest. For a multi-arch image the pinned digest is the index, which the client follows to a platform manifest, so the comparison could never match; worse, serde re-serialization does not reproduce the registry's stored bytes, so the computed digest was fictional and pinned pulls failed with a phantom mismatch. The oci-client already validates the requested digest against the raw manifest bytes on fetch (index and resolved platform manifest both), so the redundant re-check only introduced the bug. Drop it and the orphaned manifest_bytes helper. Fixes digest-pinned pulls, the sandbox baseImage pin, and lns run @sha256:… on multi-arch images.
…iew feedback) Addresses lns-registry-author review on PR #61: - model mounts unwrap the envelope spec, delivering {provider, model, parameters?} at /etc/agent/model per the pinned ARTIFACTS.md consume contract (mirrors the policy spec unwrap); falls back to the full blob when there is no spec. - bundle resolution consumes the new components.filesets key so a fileset the agent needs can live in the bundle; lns run --mount stays an optional override. resolve_mount already handles the fileset family. - document that mount read_only is advisory today (immutable composefs base; per-file RO + overlay-CoW-shadow enforcement is future work). - note that layer symlink targets are intentionally left verbatim — they resolve only inside the guest FS and the entry path is safe_rel_path-guarded.
|
Thanks both — addressed in 8821dd5, with one item tracked as a deliberate follow-up. @msa0311 (review points)
Bundle @jansavYou're right — #66 already ships |
…olicy-artifacts # Conflicts: # crates/lns-cli/src/cli.rs # crates/lns-cli/src/lib.rs # crates/lns-cli/src/service/orchestrator.rs # crates/lns-cli/tests/behaviours/world.rs # crates/lns-ipc/src/lib.rs # crates/lns-ipc/src/protocol.rs # crates/lns-policy/src/credentials.rs # crates/lns-policy/src/lib.rs # crates/lns-policy/src/registry_auth.rs # crates/lns-service/src/image/mod.rs # crates/lns-service/src/image/real.rs # crates/lns-service/src/ipc/mod.rs # scripts/coverage-floor.sh
…rge gaps Integration follow-ups after merging main: - run_push/run_pull (socket-wiring command entry points) move to registry/real.rs alongside RealRegistryClient (IGNORE'd leaf), matching how login/real.rs holds run_login/run_logout; registry/mod.rs keeps the pure push/pull + augment/SPEC wiring at 100%. - add tests for the absolute --content path, the push/pull SPEC registration, the model spec-unwrap, and an empty-rel tar entry; restructure the model test to drop an unreachable let-else panic arm.
|
Integrated current @jansav — our duplicate login is gone: dropped Also ported our Full local gate is green (lint + complexity + 100% coverage, all crates); CI is running. Ready for another look. 🙏 |
…olicy-artifacts # Conflicts: # crates/lns-cli/src/cli.rs
…latform coverage The multi-line matches! in materialize_mounts_writes_a_model_spec_at_its_path source-mapped to an unexecuted line on Linux (covered on macOS), leaving artifact.rs at 99.84% in CI. Importing RuntimeSource shortens it to a single executed line that registers as hit on both platforms.
Client integration between
lnsand the lns-registry: push/pull every artifact family and container images through one Docker-like verb pair, and run an agent or bundle artifact directly from the registry — including mounting application-layer artifacts (filesets) into the agent. (Registry login/logout is provided by #66, already onmain; this PR builds on it.)Commands
lns push <source> <ref> [--family X] [--content <dir|file>]—sourceis a local file → pushed as the typed artifact for the family (inferred from the ref's…/<segment>/…path or--family);sourceis an image ref → re-pushes an image from lns's cache.--contentpacks a local dir/file into the artifact's OCI layer (fileset authoring).lns pull <ref> [-o file]— the registry'sartifactType/config mediaType decides: a typed artifact is written out (file or stdout); an OCI image is pulled into the cache.lns run <agent-ref>/lns run <bundle-ref>[--mount <ref>[:/path]] — resolve a typed agent or bundle artifact into a concrete run and boot it;--mountattaches an application-layer artifact at the path it declares (see Running artifacts).lns run/ image pull now authenticate to the registry too (stored credential + loopback-HTTP).Generic, file-based: the registry is the schema authority (validates the config blob,
MANIFEST_INVALIDon failure). All 10 families ride one code path; no per-family Rust models.lns policy allow/deny/list/remove(local editing) unchanged.Running artifacts
lns run <ref>accepts a typed agent or bundle artifact, not just an OCI image:image,command, and full run config —user,ports,volumes— plus its declared credentials.kind: AgentSystem) → resolves its single agent component (host-qualifying the ref against the bundle's registry), applies the sandbox profile (cpus/memory), and materializes the bundle's egress policy into a run-scoped ephemeral file, leaving the project'slns-policy.yamluntouched.--mount <ref>[:/path]→ attaches an application-layer artifact (fileset/model/tool/knowledge) at the path the artifact declares in itsmount(or an explicit:/pathoverride). Now an optional override: a fileset the agent needs can ride in the bundle viacomponents.filesets(below), solns run <bundle>is self-contained. Explicit mounts coexist with bundle-resolved ones.RunArgswith explicit CLI flags always winning (--cpus,-p,-v,--cmd,--policy, …).lns connect …warning (the sandbox still asks at the boundary) — not a hard block.Typed-artifact schema v2 (two-class model + mounts)
Adopts lns-registry PR #5's redesigned schema (clean break, v1alpha1). Every family is runtime-layer (applied around the agent) or application-layer (mounted into the agent filesystem). Shared envelope
{apiVersion, kind, metadata, mount?, spec}— all family content nests underspec.model+fileset;agentdropsisolation(sandbox owns it); refs becomeArtifactRef {ref, digest?}; bundlecomponentsgainsmodelandfilesets.lns push --contentwalks a local dir/file into a single gzip-tar layer and threads it throughRegistryClient::push_artifact→ thePushArtifactIPC frame → the service'sArtifactRegistry→ the OCI manifest, so a fileset's content lands in the registry as real image layers.RuntimeFileSpec→composefs rail.modelwrites its envelopespec({provider, model, parameters?}) at/etc/agent/modelper theARTIFACTS.mdconsume contract (mirrors the policy unwrap);tool/knowledge/filesethave their OCI layers fetched (pull_artifact_layers), gz-sniffed, tar-walked (tar-slip guarded bysafe_rel_path), and injected under/etc/agent/tools|knowledge/<name>(or the fileset's explicitmount.path). The thin CLI resolver computes the mount path from each component's config blob and emitsRunImageArgs.artifact_mounts {reference, path, read_only}; the service does the pull + untar at boot.read_onlyis advisory today (the seed lands in the read-only composefs base; per-file RO + overlay-CoW-shadow enforcement is future work).materialize_policyunwraps the policy envelope'sspecand rewrites eachspec.integrationsArtifactRef({ref}) to the bare integration id the supervisor reads, so a bundle policy that references integrations loads instead of failing on a type mismatch.Architecture
artifact: family taxonomy (slug / media types / path-segment / inference, two-classis_application_layer) + YAML-or-JSON → JSONto_config_blob; typed deserializers (the registry-contract crate owns the schemas).PushArtifact {…, layers}/PushImage/Pull;ArtifactMount {reference, path, read_only}onRunImageArgs/RunConfig.artifact: auth resolution + loopback/LNS_REGISTRY_PLAIN_HTTPprotocol; artifact-vs-image decided from the manifest; image push reconstructs from the persisted image record;materialize_mountspulls + expands application-layer layers intoRuntimeFileSpecs folded into the runtime layer.registry+run::resolve: thin IPC client; the resolver is a pure, injectable unit driven through theRegistryClientport (fully host-tested with a fake registry, no real I/O).CLI stays a thin IPC client; the daemon owns the
oci-client. Loopback registries (localhost/127.0.0.1/::1) auto-use plain HTTP, like Docker.Tests / gate
Full local gate green (
make lint && make complexity && make coverage, all touched crates 100%, 0 failures). Layer 2:registry.featureround-trips artifacts + image push + family inference;run_agent_ref.feature+run_bundle_ref.featurepin resolution/credential gate/ephemeral policy;run_mount_flag.featurepins--mountlanding anArtifactMountat the fileset's declared path (and an explicit override). Layer 3 covers the family contract, deserializers, blob conversion, the resolver (mapping + overlay precedence + every error path), the content-layer packer, the integration-ref normalizer, service materialization (gz-sniff, tar-walk, tar-slip guard), and the IPC codec. Coverage IGNOREs: the thin IPC leaves (registry/real.rs) and the OCI leaf inimage/real.rs.Live end-to-end verification (PR#5 registry + microVM)
Migrated the hermes demo (
hermes-agent/lens/) to schema v2 and a config fileset: hermes is non-compliant (reads only$HERMES_HOME/config.yaml, never/etc/agent/*), so itsconfig.yamlships as a fileset mounted at/opt/data, replacing the oldcp/sedconfig shim. Verified against the live registry + a real microVM:lns push fileset-config.yaml …/filesets/hermes-config:v1 --content filesets/hermes-config→ accepted as142 bytes + 1 layer; the registry manifest carries the gzip-tar layer; pulled back, it containsconfig.yaml.lns run …/bundles/hermes-system:v1 --mount …/filesets/hermes-config:v1→ resolved the agent, applied the sandbox (2 vCPU / 3072 MiB) and the materialized egress policy (16 allow rules, integration refs normalized), mounted the fileset at/opt/data, booted, and hermes came up reading the fileset config — its banner showedclaude-sonnet-4-6(the fileset's value, not the image defaultclaude-opus-4.6), andapi.anthropic.comtraffic showed theanthropicintegration injecting its credential.Digest-pin fix (
pull_inner). Digest-pinned pulls previously failed on a phantom mismatch:pull_innerre-hashed a serde re-serialization of the manifest and compared it to the requested digest, which (a) never matches for a multi-arch image — the pin is the index, which the client follows to a platform manifest — and (b) doesn't reproduce the registry's stored bytes, so the computed digest was fictional. The oci-client already verifies the requested digest against the raw bytes on fetch (index + resolved platform manifest), so the redundant re-check was dropped. Digest-pinned pulls, the sandboxbaseImagepin, andlns run @sha256:…on multi-arch images now work.Integrated with current main ✅
Merged current
mainand reconciled the divergence: dropped our duplicate login in favour of #66 (deletedauth/mod.rs+ our collidingregistry_auth.rs; adopted main'sRegistryAuthStore/credential_forand rewired the service-side auth helpers to it), and portedpush/pull/run --mountinto main'sCommandSpecregistry (each is now a*_SPECwith the socket-wiring entry points in therealleaf, mirroringlogin). Reconciled main's host-bind feature (RunImageArgs.name/binds, the unifiedmounts: Vec<MountSpec>run flag) alongside ourartifact_mounts, and kept ourpull_innerdigest fix (main still carried the bug). Full gate green (lint + complexity + 100% coverage across all crates); the branch isMERGEABLE.Deferred (TODO)
GET /ext/v1/types(today: rely on serverMANIFEST_INVALID).