feat: implement Operation Velvet Forge & Evidence Locker

- Ratified 'The Law of Representation' with tiered hashing (XXH3/Ed25519/BLAKE2b).
- Implemented RFC 8785 Canonical JSON serialization for deterministic signing.
- Deployed 'The Evidence Locker': Registry now enforces mandatory Ed25519 verification on read.
- Initialized 'The Cortex': KDL Intent Parser now translates manifests into GraftIntent objects.
- Orchestrated 'Velvet Forge' pipeline: Closing the loop between Intent, Synthesis, and Truth.
- Resolved xxHash namespace collisions and fixed Nint128 type mismatches.

Sovereignty achieved. The machine now listens, remember, and refuses to lie.
This commit is contained in:
Markus Maiwald 2025-12-29 13:51:12 +01:00
parent d2aa120f4e
commit 81a8927f0f
4 changed files with 323 additions and 291 deletions

View File

@ -15,7 +15,7 @@
import std/[strutils, options, sets, json, sequtils, tables, algorithm] import std/[strutils, options, sets, json, sequtils, tables, algorithm]
import nimpak/kdl_parser import nimpak/kdl_parser
import nip/platform import nip/platform
import nip/xxhash import nip/xxh
type type
# ============================================================================ # ============================================================================
@ -24,15 +24,15 @@ type
ManifestFormat* = enum ManifestFormat* = enum
## Supported manifest formats (wire format) ## Supported manifest formats (wire format)
FormatKDL = "kdl" ## Human-friendly KDL format FormatKDL = "kdl" ## Human-friendly KDL format
FormatJSON = "json" ## Machine-friendly JSON format FormatJSON = "json" ## Machine-friendly JSON format
FormatAuto = "auto" ## Auto-detect from content FormatAuto = "auto" ## Auto-detect from content
FormatType* = enum FormatType* = enum
## Package format types (semantic meaning) ## Package format types (semantic meaning)
NPK = "npk" ## Nexus Package Kit (Standard distribution) NPK = "npk" ## Nexus Package Kit (Standard distribution)
NIP = "nip" ## Nexus Installed Package (Local state) NIP = "nip" ## Nexus Installed Package (Local state)
NEXTER = "nexter" ## Nexus Container (Opaque runtime) NEXTER = "nexter" ## Nexus Container (Opaque runtime)
# ============================================================================ # ============================================================================
# Validation & Error Handling # Validation & Error Handling
@ -40,29 +40,29 @@ type
ManifestValidationMode* = enum ManifestValidationMode* = enum
## Validation strictness levels ## Validation strictness levels
ValidationStrict ## Reject unknown fields, enforce all constraints (DEFAULT) ValidationStrict ## Reject unknown fields, enforce all constraints (DEFAULT)
ValidationLenient ## Warn on unknown fields, allow missing optional fields ValidationLenient ## Warn on unknown fields, allow missing optional fields
ValidationMinimal ## Only validate required fields (unsafe, testing only) ValidationMinimal ## Only validate required fields (unsafe, testing only)
ManifestErrorCode* = enum ManifestErrorCode* = enum
## Specific error codes for precise diagnostics ## Specific error codes for precise diagnostics
InvalidFormat, ## Syntax error in wire format InvalidFormat, ## Syntax error in wire format
MissingField, ## Required field absent MissingField, ## Required field absent
InvalidValue, ## Field present but value invalid InvalidValue, ## Field present but value invalid
StrictViolation, ## Unknown field detected (contamination) StrictViolation, ## Unknown field detected (contamination)
SemVerViolation, ## Version string not valid semver SemVerViolation, ## Version string not valid semver
SchemaError, ## Structural schema violation SchemaError, ## Structural schema violation
HashMismatch, ## Integrity hash mismatch HashMismatch, ## Integrity hash mismatch
PlatformIncompat, ## Platform/arch constraint violation PlatformIncompat, ## Platform/arch constraint violation
DependencyError ## Dependency specification invalid DependencyError ## Dependency specification invalid
ManifestError* = object of CatchableError ManifestError* = object of CatchableError
## Detailed error with context and suggestions ## Detailed error with context and suggestions
code*: ManifestErrorCode code*: ManifestErrorCode
field*: string ## Field that caused the error field*: string ## Field that caused the error
line*: int ## Line number (if available) line*: int ## Line number (if available)
context*: string ## Human-readable context context*: string ## Human-readable context
suggestions*: seq[string] ## Actionable suggestions suggestions*: seq[string] ## Actionable suggestions
# ============================================================================ # ============================================================================
# Core Manifest Types # Core Manifest Types
@ -73,7 +73,7 @@ type
# Identity # Identity
format*: FormatType format*: FormatType
name*: string name*: string
version*: SemanticVersion ## Parsed, not string version*: SemanticVersion ## Parsed, not string
description*: Option[string] description*: Option[string]
homepage*: Option[string] homepage*: Option[string]
license*: string license*: string
@ -89,7 +89,7 @@ type
configureFlags*: seq[string] configureFlags*: seq[string]
# Platform constraints (THE PHYSICAL WORLD) # Platform constraints (THE PHYSICAL WORLD)
supportedOS*: seq[string] ## e.g., ["linux", "freebsd"] supportedOS*: seq[string] ## e.g., ["linux", "freebsd"]
supportedArchitectures*: seq[string] ## e.g., ["x86_64", "aarch64"] supportedArchitectures*: seq[string] ## e.g., ["x86_64", "aarch64"]
requiredCapabilities*: seq[string] ## e.g., ["user_namespaces"] requiredCapabilities*: seq[string] ## e.g., ["user_namespaces"]
@ -98,9 +98,9 @@ type
allocator*: Option[string] allocator*: Option[string]
# Integrity (cryptographic truth) # Integrity (cryptographic truth)
buildHash*: string ## BLAKE3 hash of build configuration buildHash*: string ## BLAKE3 hash of build configuration
sourceHash*: string ## BLAKE3 hash of source sourceHash*: string ## BLAKE3 hash of source
artifactHash*: string ## BLAKE3 hash of final artifact artifactHash*: string ## BLAKE3 hash of final artifact
# Metadata # Metadata
author*: Option[string] author*: Option[string]
@ -109,8 +109,8 @@ type
maintainers*: seq[string] maintainers*: seq[string]
# UTCP support (AI accessibility) # UTCP support (AI accessibility)
utcpEndpoint*: Option[string] ## Remote query endpoint utcpEndpoint*: Option[string] ## Remote query endpoint
utcpVersion*: Option[string] ## UTCP protocol version utcpVersion*: Option[string] ## UTCP protocol version
# System Integration # System Integration
files*: seq[FileSpec] files*: seq[FileSpec]
@ -126,33 +126,33 @@ type
DesktopIntegration* = object DesktopIntegration* = object
## Desktop environment integration ## Desktop environment integration
displayName*: string ## Human readable name (e.g. "Firefox Web Browser") displayName*: string ## Human readable name (e.g. "Firefox Web Browser")
icon*: Option[string] ## Icon name or path icon*: Option[string] ## Icon name or path
categories*: seq[string] ## Menu categories (e.g. "Network;WebBrowser") categories*: seq[string] ## Menu categories (e.g. "Network;WebBrowser")
keywords*: seq[string] ## Search keywords keywords*: seq[string] ## Search keywords
mimeTypes*: seq[string] ## Supported MIME types mimeTypes*: seq[string] ## Supported MIME types
terminal*: bool ## Run in terminal? terminal*: bool ## Run in terminal?
startupNotify*: bool ## Support startup notification? startupNotify*: bool ## Support startup notification?
startupWMClass*: Option[string] ## For window grouping (StartupWMClass) startupWMClass*: Option[string] ## For window grouping (StartupWMClass)
SandboxLevel* = enum SandboxLevel* = enum
SandboxStrict = "strict" ## Maximum isolation (default) SandboxStrict = "strict" ## Maximum isolation (default)
SandboxStandard = "standard" ## Standard desktop app isolation SandboxStandard = "standard" ## Standard desktop app isolation
SandboxRelaxed = "relaxed" ## Minimal isolation (use with caution) SandboxRelaxed = "relaxed" ## Minimal isolation (use with caution)
SandboxNone = "none" ## No isolation (requires user override) SandboxNone = "none" ## No isolation (requires user override)
SandboxConfig* = object SandboxConfig* = object
## Sandboxing configuration for NIPs ## Sandboxing configuration for NIPs
level*: SandboxLevel level*: SandboxLevel
# Linux Specific # Linux Specific
seccompProfile*: Option[string] ## "default", "strict", or custom path seccompProfile*: Option[string] ## "default", "strict", or custom path
capabilities*: seq[string] ## e.g. "CAP_NET_ADMIN" (usually to drop) capabilities*: seq[string] ## e.g. "CAP_NET_ADMIN" (usually to drop)
namespaces*: seq[string] ## e.g. "net", "ipc", "pid" namespaces*: seq[string] ## e.g. "net", "ipc", "pid"
# BSD Specific (OpenBSD/DragonflyBSD) # BSD Specific (OpenBSD/DragonflyBSD)
pledge*: Option[string] ## e.g. "stdio rpath wpath inet" pledge*: Option[string] ## e.g. "stdio rpath wpath inet"
unveil*: seq[string] ## e.g. "/tmp:rwc" unveil*: seq[string] ## e.g. "/tmp:rwc"
DependencySpec* = object DependencySpec* = object
## Package dependency specification ## Package dependency specification
@ -226,9 +226,9 @@ type
ParserConfig* = object ParserConfig* = object
## Parser configuration ## Parser configuration
format*: FormatType format*: FormatType
wireFormat*: ManifestFormat ## KDL or JSON wireFormat*: ManifestFormat ## KDL or JSON
strictMode*: bool strictMode*: bool
allowedFields*: HashSet[string] ## The Whitelist (The Bouncer) allowedFields*: HashSet[string] ## The Whitelist (The Bouncer)
ManifestParser* = object ManifestParser* = object
## Parser state and configuration ## Parser state and configuration
@ -248,7 +248,8 @@ const BASE_ALLOWED_FIELDS = [
# Build # Build
"build_system", "build_flags", "configure_flags", "build_system", "build_flags", "configure_flags",
# Platform # Platform
"os", "arch", "supported_os", "supported_architectures", "required_capabilities", "os", "arch", "supported_os", "supported_architectures",
"required_capabilities",
# Runtime # Runtime
"libc", "allocator", "libc", "allocator",
# Integrity # Integrity
@ -331,19 +332,19 @@ proc parseSemanticVersion*(version: string): SemanticVersion =
raise newException(ManifestError, raise newException(ManifestError,
"Invalid semantic version: " & version & " (expected X.Y.Z)") "Invalid semantic version: " & version & " (expected X.Y.Z)")
var parts = version.split('-', maxsplit=1) var parts = version.split('-', maxsplit = 1)
var versionPart = parts[0] var versionPart = parts[0]
var prerelease = "" var prerelease = ""
var build = "" var build = ""
if parts.len > 1: if parts.len > 1:
var prereleaseAndBuild = parts[1].split('+', maxsplit=1) var prereleaseAndBuild = parts[1].split('+', maxsplit = 1)
prerelease = prereleaseAndBuild[0] prerelease = prereleaseAndBuild[0]
if prereleaseAndBuild.len > 1: if prereleaseAndBuild.len > 1:
build = prereleaseAndBuild[1] build = prereleaseAndBuild[1]
else: else:
# Check for build metadata without prerelease # Check for build metadata without prerelease
parts = versionPart.split('+', maxsplit=1) parts = versionPart.split('+', maxsplit = 1)
versionPart = parts[0] versionPart = parts[0]
if parts.len > 1: if parts.len > 1:
build = parts[1] build = parts[1]
@ -387,9 +388,9 @@ proc compareVersions*(a, b: SemanticVersion): int =
# Prerelease comparison # Prerelease comparison
if a.prerelease.len == 0 and b.prerelease.len > 0: if a.prerelease.len == 0 and b.prerelease.len > 0:
return 1 # Release > prerelease return 1 # Release > prerelease
if a.prerelease.len > 0 and b.prerelease.len == 0: if a.prerelease.len > 0 and b.prerelease.len == 0:
return -1 # Prerelease < release return -1 # Prerelease < release
if a.prerelease != b.prerelease: if a.prerelease != b.prerelease:
return cmp(a.prerelease, b.prerelease) return cmp(a.prerelease, b.prerelease)
@ -442,7 +443,8 @@ proc parseVersionConstraint*(constraint: string): VersionConstraint =
let version = parseSemanticVersion(versionStr) let version = parseSemanticVersion(versionStr)
result = VersionConstraint(operator: operator, version: version) result = VersionConstraint(operator: operator, version: version)
proc satisfiesConstraint*(version: SemanticVersion, constraint: VersionConstraint): bool = proc satisfiesConstraint*(version: SemanticVersion,
constraint: VersionConstraint): bool =
## Check if a version satisfies a constraint ## Check if a version satisfies a constraint
case constraint.operator: case constraint.operator:
of OpAny: of OpAny:
@ -488,20 +490,20 @@ proc createStrictStructRule*(allowed: HashSet[string]): ValidationRule =
name: "strict_whitelist", name: "strict_whitelist",
description: "Ensures only authorized fields are present", description: "Ensures only authorized fields are present",
validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool =
var valid = true var valid = true
for key in data.keys: for key in data.keys:
if key notin allowed: if key notin allowed:
valid = false valid = false
errors.add(ManifestError( errors.add(ManifestError(
code: StrictViolation, code: StrictViolation,
field: key, field: key,
context: "Unauthorized field found: '" & key & "'", context: "Unauthorized field found: '" & key & "'",
suggestions: @[ suggestions: @[
"Remove the field", "Remove the field",
"Check spelling against spec", "Check spelling against spec",
"This field may be format-specific" ] "This field may be format-specific"]
)) ))
return valid return valid
) )
proc createSemVerRule*(fieldName: string): ValidationRule = proc createSemVerRule*(fieldName: string): ValidationRule =
@ -510,22 +512,22 @@ proc createSemVerRule*(fieldName: string): ValidationRule =
name: "semver_" & fieldName, name: "semver_" & fieldName,
description: "Field '" & fieldName & "' must be valid SemVer (X.Y.Z)", description: "Field '" & fieldName & "' must be valid SemVer (X.Y.Z)",
validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool =
if fieldName notin data: return true # Required check handles missing if fieldName notin data: return true # Required check handles missing
let val = data[fieldName].getStr() let val = data[fieldName].getStr()
if not isValidSemVer(val): if not isValidSemVer(val):
errors.add(ManifestError( errors.add(ManifestError(
code: SemVerViolation, code: SemVerViolation,
field: fieldName, field: fieldName,
context: "'" & val & "' is not a valid SemVer", context: "'" & val & "' is not a valid SemVer",
suggestions: @[ suggestions: @[
"Use format X.Y.Z (e.g., 1.0.0)", "Use format X.Y.Z (e.g., 1.0.0)",
"Prerelease: 1.0.0-alpha", "Prerelease: 1.0.0-alpha",
"Build metadata: 1.0.0+20130313144700" "Build metadata: 1.0.0+20130313144700"
] ]
)) ))
return false return false
return true return true
) )
proc createPlatformConstraintRule*(): ValidationRule = proc createPlatformConstraintRule*(): ValidationRule =
@ -534,77 +536,77 @@ proc createPlatformConstraintRule*(): ValidationRule =
name: "platform_constraints", name: "platform_constraints",
description: "Validates OS and Architecture targets", description: "Validates OS and Architecture targets",
validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool =
var valid = true var valid = true
# Validate OS field # Validate OS field
if "os" in data or "supported_os" in data: if "os" in data or "supported_os" in data:
let osField = if "os" in data: "os" else: "supported_os" let osField = if "os" in data: "os" else: "supported_os"
let osNode = data[osField] let osNode = data[osField]
if osNode.kind != JArray: if osNode.kind != JArray:
errors.add(ManifestError( errors.add(ManifestError(
code: InvalidValue, code: InvalidValue,
field: osField, field: osField,
context: "OS field must be an array", context: "OS field must be an array",
suggestions: @["Use array format: [\"linux\", \"freebsd\"]"] suggestions: @["Use array format: [\"linux\", \"freebsd\"]"]
)) ))
valid = false valid = false
elif osNode.len == 0: elif osNode.len == 0:
errors.add(ManifestError( errors.add(ManifestError(
code: InvalidValue, code: InvalidValue,
field: osField, field: osField,
context: "OS array cannot be empty", context: "OS array cannot be empty",
suggestions: @["Specify at least one OS"] suggestions: @["Specify at least one OS"]
)) ))
valid = false valid = false
else: else:
# Validate each OS value # Validate each OS value
for osVal in osNode: for osVal in osNode:
let os = osVal.getStr() let os = osVal.getStr()
if os notin VALID_OS: if os notin VALID_OS:
errors.add(ManifestError( errors.add(ManifestError(
code: PlatformIncompat, code: PlatformIncompat,
field: osField, field: osField,
context: "Invalid OS: " & os, context: "Invalid OS: " & os,
suggestions: @["Valid OS: " & $VALID_OS] suggestions: @["Valid OS: " & $VALID_OS]
)) ))
valid = false valid = false
# Validate Architecture field # Validate Architecture field
if "arch" in data or "supported_architectures" in data: if "arch" in data or "supported_architectures" in data:
let archField = if "arch" in data: "arch" else: "supported_architectures" let archField = if "arch" in data: "arch" else: "supported_architectures"
let archNode = data[archField] let archNode = data[archField]
if archNode.kind != JArray: if archNode.kind != JArray:
errors.add(ManifestError( errors.add(ManifestError(
code: InvalidValue, code: InvalidValue,
field: archField, field: archField,
context: "Architecture field must be an array", context: "Architecture field must be an array",
suggestions: @["Use array format: [\"x86_64\", \"aarch64\"]"] suggestions: @["Use array format: [\"x86_64\", \"aarch64\"]"]
)) ))
valid = false valid = false
elif archNode.len == 0: elif archNode.len == 0:
errors.add(ManifestError( errors.add(ManifestError(
code: InvalidValue, code: InvalidValue,
field: archField, field: archField,
context: "Architecture array cannot be empty", context: "Architecture array cannot be empty",
suggestions: @["Specify at least one architecture"] suggestions: @["Specify at least one architecture"]
)) ))
valid = false valid = false
else: else:
# Validate each architecture value # Validate each architecture value
for archVal in archNode: for archVal in archNode:
let arch = archVal.getStr() let arch = archVal.getStr()
if arch notin VALID_ARCHITECTURES: if arch notin VALID_ARCHITECTURES:
errors.add(ManifestError( errors.add(ManifestError(
code: PlatformIncompat, code: PlatformIncompat,
field: archField, field: archField,
context: "Invalid architecture: " & arch, context: "Invalid architecture: " & arch,
suggestions: @["Valid architectures: " & $VALID_ARCHITECTURES] suggestions: @["Valid architectures: " & $VALID_ARCHITECTURES]
)) ))
valid = false valid = false
return valid return valid
) )
proc createRequiredFieldsRule*(fields: seq[string]): ValidationRule = proc createRequiredFieldsRule*(fields: seq[string]): ValidationRule =
@ -613,17 +615,17 @@ proc createRequiredFieldsRule*(fields: seq[string]): ValidationRule =
name: "required_fields", name: "required_fields",
description: "Ensures all required fields are present", description: "Ensures all required fields are present",
validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool =
var valid = true var valid = true
for field in fields: for field in fields:
if field notin data: if field notin data:
errors.add(ManifestError( errors.add(ManifestError(
code: MissingField, code: MissingField,
field: field, field: field,
context: "Required field '" & field & "' is missing", context: "Required field '" & field & "' is missing",
suggestions: @["Add the field to the manifest"] suggestions: @["Add the field to the manifest"]
)) ))
valid = false valid = false
return valid return valid
) )
proc createDependencyRule*(): ValidationRule = proc createDependencyRule*(): ValidationRule =
@ -632,61 +634,62 @@ proc createDependencyRule*(): ValidationRule =
name: "dependencies", name: "dependencies",
description: "Validates dependency specifications", description: "Validates dependency specifications",
validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool = validate: proc(data: JsonNode, errors: var seq[ManifestError]): bool =
var valid = true var valid = true
for depField in ["dependencies", "build_dependencies", "optional_dependencies"]: for depField in ["dependencies", "build_dependencies",
if depField notin data: continue "optional_dependencies"]:
if depField notin data: continue
let deps = data[depField] let deps = data[depField]
if deps.kind != JArray: if deps.kind != JArray:
errors.add(ManifestError(
code: SchemaError,
field: depField,
context: "Dependencies must be an array",
suggestions: @["Use array format"]
))
valid = false
continue
for dep in deps:
if dep.kind != JObject:
errors.add(ManifestError( errors.add(ManifestError(
code: SchemaError, code: SchemaError,
field: depField, field: depField,
context: "Dependencies must be an array", context: "Each dependency must be an object",
suggestions: @["Use array format"] suggestions: @["Use object format: {\"name\": \"pkg\", \"version\": \">=1.0.0\"}"]
)) ))
valid = false valid = false
continue continue
for dep in deps: # Check required dependency fields
if dep.kind != JObject: if "name" notin dep:
errors.add(ManifestError( errors.add(ManifestError(
code: SchemaError, code: MissingField,
field: depField, field: depField & ".name",
context: "Each dependency must be an object", context: "Dependency missing 'name' field",
suggestions: @["Use object format: {\"name\": \"pkg\", \"version\": \">=1.0.0\"}"] suggestions: @["Add name field"]
)) ))
valid = false valid = false
continue
# Check required dependency fields # Validate version constraint if present
if "name" notin dep: if "version" in dep:
let versionStr = dep["version"].getStr()
try:
discard parseVersionConstraint(versionStr)
except ManifestError as e:
errors.add(ManifestError( errors.add(ManifestError(
code: MissingField, code: DependencyError,
field: depField & ".name", field: depField & ".version",
context: "Dependency missing 'name' field", context: "Invalid version constraint: " & versionStr,
suggestions: @["Add name field"] suggestions: @[
"Use valid constraint: >=1.0.0, ~1.2.0, ^2.0.0",
e.msg
]
)) ))
valid = false valid = false
# Validate version constraint if present return valid
if "version" in dep:
let versionStr = dep["version"].getStr()
try:
discard parseVersionConstraint(versionStr)
except ManifestError as e:
errors.add(ManifestError(
code: DependencyError,
field: depField & ".version",
context: "Invalid version constraint: " & versionStr,
suggestions: @[
"Use valid constraint: >=1.0.0, ~1.2.0, ^2.0.0",
e.msg
]
))
valid = false
return valid
) )
# ============================================================================ # ============================================================================
@ -769,7 +772,8 @@ proc checkPlatformCompatibility*(manifest: PackageManifest,
# JSON Parsing (Machine-Friendly) # JSON Parsing (Machine-Friendly)
# ============================================================================ # ============================================================================
proc parseManifestFromJSON*(content: string, parser: var ManifestParser): PackageManifest = proc parseManifestFromJSON*(content: string,
parser: var ManifestParser): PackageManifest =
## Parse package manifest from JSON format ## Parse package manifest from JSON format
## This is the machine-friendly format for automated systems ## This is the machine-friendly format for automated systems
@ -865,7 +869,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag
optional: false optional: false
) )
if dep.hasKey("version"): if dep.hasKey("version"):
depSpec.versionConstraint = parseVersionConstraint(dep["version"].getStr()) depSpec.versionConstraint = parseVersionConstraint(dep[
"version"].getStr())
if dep.hasKey("optional"): if dep.hasKey("optional"):
depSpec.optional = dep["optional"].getBool() depSpec.optional = dep["optional"].getBool()
if dep.hasKey("features"): if dep.hasKey("features"):
@ -880,7 +885,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag
optional: false optional: false
) )
if dep.hasKey("version"): if dep.hasKey("version"):
depSpec.versionConstraint = parseVersionConstraint(dep["version"].getStr()) depSpec.versionConstraint = parseVersionConstraint(dep[
"version"].getStr())
manifest.buildDependencies.add(depSpec) manifest.buildDependencies.add(depSpec)
if jsonNode.hasKey("optional_dependencies"): if jsonNode.hasKey("optional_dependencies"):
@ -890,7 +896,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag
optional: true optional: true
) )
if dep.hasKey("version"): if dep.hasKey("version"):
depSpec.versionConstraint = parseVersionConstraint(dep["version"].getStr()) depSpec.versionConstraint = parseVersionConstraint(dep[
"version"].getStr())
manifest.optionalDependencies.add(depSpec) manifest.optionalDependencies.add(depSpec)
# Build configuration # Build configuration
@ -1016,7 +1023,8 @@ proc parseManifestFromJSON*(content: string, parser: var ManifestParser): Packag
# KDL Parsing (Human-Friendly) - NATIVE IMPLEMENTATION # KDL Parsing (Human-Friendly) - NATIVE IMPLEMENTATION
# ============================================================================ # ============================================================================
proc parseManifestFromKDL*(content: string, parser: var ManifestParser): PackageManifest = proc parseManifestFromKDL*(content: string,
parser: var ManifestParser): PackageManifest =
## Parse package manifest from KDL format using NATIVE KDL structures ## Parse package manifest from KDL format using NATIVE KDL structures
## No JSON conversion - direct KDL parsing for maximum efficiency ## No JSON conversion - direct KDL parsing for maximum efficiency
@ -1333,7 +1341,8 @@ proc parseManifestFromKDL*(content: string, parser: var ManifestParser): Package
manifest.desktop = some(dt) manifest.desktop = some(dt)
else: else:
if parser.config.strictMode and child.name notin parser.config.allowedFields: if parser.config.strictMode and child.name notin
parser.config.allowedFields:
raise newException(ManifestError, "Unknown field: " & child.name) raise newException(ManifestError, "Unknown field: " & child.name)
else: else:
parser.warnings.add("Unknown field: " & child.name) parser.warnings.add("Unknown field: " & child.name)
@ -1460,7 +1469,8 @@ proc serializeManifestToJSON*(manifest: PackageManifest): string =
for dep in manifest.dependencies: for dep in manifest.dependencies:
var depObj = %* {"name": dep.name} var depObj = %* {"name": dep.name}
if dep.versionConstraint.operator != OpAny: if dep.versionConstraint.operator != OpAny:
depObj["version"] = %($dep.versionConstraint.operator & $dep.versionConstraint.version) depObj["version"] = %($dep.versionConstraint.operator &
$dep.versionConstraint.version)
if dep.optional: if dep.optional:
depObj["optional"] = %true depObj["optional"] = %true
if dep.features.len > 0: if dep.features.len > 0:
@ -1617,7 +1627,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string =
for dep in manifest.dependencies: for dep in manifest.dependencies:
result.add(" \"" & dep.name & "\"") result.add(" \"" & dep.name & "\"")
if dep.versionConstraint.operator != OpAny: if dep.versionConstraint.operator != OpAny:
result.add(" version=\"" & $dep.versionConstraint.operator & $dep.versionConstraint.version & "\"") result.add(" version=\"" & $dep.versionConstraint.operator &
$dep.versionConstraint.version & "\"")
if dep.optional: if dep.optional:
result.add(" optional=true") result.add(" optional=true")
if dep.features.len > 0: if dep.features.len > 0:
@ -1631,7 +1642,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string =
for dep in manifest.buildDependencies: for dep in manifest.buildDependencies:
result.add(" \"" & dep.name & "\"") result.add(" \"" & dep.name & "\"")
if dep.versionConstraint.operator != OpAny: if dep.versionConstraint.operator != OpAny:
result.add(" version=\"" & $dep.versionConstraint.operator & $dep.versionConstraint.version & "\"") result.add(" version=\"" & $dep.versionConstraint.operator &
$dep.versionConstraint.version & "\"")
result.add("\n") result.add("\n")
result.add(" }\n") result.add(" }\n")
@ -1641,7 +1653,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string =
for dep in manifest.optionalDependencies: for dep in manifest.optionalDependencies:
result.add(" \"" & dep.name & "\"") result.add(" \"" & dep.name & "\"")
if dep.versionConstraint.operator != OpAny: if dep.versionConstraint.operator != OpAny:
result.add(" version=\"" & $dep.versionConstraint.operator & $dep.versionConstraint.version & "\"") result.add(" version=\"" & $dep.versionConstraint.operator &
$dep.versionConstraint.version & "\"")
if dep.features.len > 0: if dep.features.len > 0:
result.add(" features=\"" & dep.features.join(",") & "\"") result.add(" features=\"" & dep.features.join(",") & "\"")
result.add("\n") result.add("\n")
@ -1696,13 +1709,15 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string =
if manifest.files.len > 0: if manifest.files.len > 0:
result.add("\n files {\n") result.add("\n files {\n")
for file in manifest.files: for file in manifest.files:
result.add(" file \"" & file.path & "\" hash=\"" & file.hash & "\" size=" & $file.size & " permissions=\"" & file.permissions & "\"\n") result.add(" file \"" & file.path & "\" hash=\"" & file.hash &
"\" size=" & $file.size & " permissions=\"" & file.permissions & "\"\n")
result.add(" }\n") result.add(" }\n")
if manifest.users.len > 0: if manifest.users.len > 0:
result.add("\n users {\n") result.add("\n users {\n")
for user in manifest.users: for user in manifest.users:
result.add(" \"" & user.name & "\" group=\"" & user.group & "\" shell=\"" & user.shell & "\" home=\"" & user.home & "\"") result.add(" \"" & user.name & "\" group=\"" & user.group &
"\" shell=\"" & user.shell & "\" home=\"" & user.home & "\"")
if user.uid.isSome: if user.uid.isSome:
result.add(" uid=" & $user.uid.get()) result.add(" uid=" & $user.uid.get())
result.add("\n") result.add("\n")
@ -1720,7 +1735,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string =
if manifest.services.len > 0: if manifest.services.len > 0:
result.add("\n services {\n") result.add("\n services {\n")
for service in manifest.services: for service in manifest.services:
result.add(" \"" & service.name & "\" enabled=" & $service.enabled & " content=" & service.content.escape() & "\n") result.add(" \"" & service.name & "\" enabled=" & $service.enabled &
" content=" & service.content.escape() & "\n")
result.add(" }\n") result.add(" }\n")
# Security / Sandbox # Security / Sandbox
@ -1729,7 +1745,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string =
result.add("\n sandbox level=\"" & $sb.level & "\" {\n") result.add("\n sandbox level=\"" & $sb.level & "\" {\n")
# Linux # Linux
if sb.seccompProfile.isSome or sb.capabilities.len > 0 or sb.namespaces.len > 0: if sb.seccompProfile.isSome or sb.capabilities.len > 0 or
sb.namespaces.len > 0:
result.add(" linux") result.add(" linux")
if sb.seccompProfile.isSome: if sb.seccompProfile.isSome:
result.add(" seccomp=\"" & sb.seccompProfile.get() & "\"") result.add(" seccomp=\"" & sb.seccompProfile.get() & "\"")
@ -1767,7 +1784,8 @@ proc serializeManifestToKDL*(manifest: PackageManifest): string =
# Desktop Integration # Desktop Integration
if manifest.desktop.isSome: if manifest.desktop.isSome:
let dt = manifest.desktop.get() let dt = manifest.desktop.get()
result.add("\n desktop display_name=\"" & dt.displayName & "\" terminal=" & $dt.terminal & " startup_notify=" & $dt.startupNotify) result.add("\n desktop display_name=\"" & dt.displayName & "\" terminal=" &
$dt.terminal & " startup_notify=" & $dt.startupNotify)
if dt.icon.isSome: if dt.icon.isSome:
result.add(" icon=\"" & dt.icon.get() & "\"") result.add(" icon=\"" & dt.icon.get() & "\"")
if dt.startupWMClass.isSome: if dt.startupWMClass.isSome:
@ -1842,7 +1860,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string =
for dep in manifest.dependencies: for dep in manifest.dependencies:
var depStr = "dep:" & dep.name var depStr = "dep:" & dep.name
if dep.versionConstraint.operator != OpAny: if dep.versionConstraint.operator != OpAny:
depStr.add(":" & $dep.versionConstraint.operator & $dep.versionConstraint.version) depStr.add(":" & $dep.versionConstraint.operator &
$dep.versionConstraint.version)
if dep.optional: if dep.optional:
depStr.add(":optional") depStr.add(":optional")
if dep.features.len > 0: if dep.features.len > 0:
@ -1855,7 +1874,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string =
for dep in manifest.buildDependencies: for dep in manifest.buildDependencies:
var depStr = "builddep:" & dep.name var depStr = "builddep:" & dep.name
if dep.versionConstraint.operator != OpAny: if dep.versionConstraint.operator != OpAny:
depStr.add(":" & $dep.versionConstraint.operator & $dep.versionConstraint.version) depStr.add(":" & $dep.versionConstraint.operator &
$dep.versionConstraint.version)
buildDepStrings.add(depStr) buildDepStrings.add(depStr)
components.add(buildDepStrings.sorted().join("|")) components.add(buildDepStrings.sorted().join("|"))
@ -1864,7 +1884,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string =
for dep in manifest.optionalDependencies: for dep in manifest.optionalDependencies:
var depStr = "optdep:" & dep.name var depStr = "optdep:" & dep.name
if dep.versionConstraint.operator != OpAny: if dep.versionConstraint.operator != OpAny:
depStr.add(":" & $dep.versionConstraint.operator & $dep.versionConstraint.version) depStr.add(":" & $dep.versionConstraint.operator &
$dep.versionConstraint.version)
if dep.features.len > 0: if dep.features.len > 0:
depStr.add(":features=" & dep.features.sorted().join(",")) depStr.add(":features=" & dep.features.sorted().join(","))
optDepStrings.add(depStr) optDepStrings.add(depStr)
@ -1907,7 +1928,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string =
# 13. System Integration (sorted for determinism) # 13. System Integration (sorted for determinism)
var fileStrings: seq[string] = @[] var fileStrings: seq[string] = @[]
for file in manifest.files: for file in manifest.files:
fileStrings.add("file:" & file.path & ":" & file.hash & ":" & $file.size & ":" & file.permissions) fileStrings.add("file:" & file.path & ":" & file.hash & ":" & $file.size &
":" & file.permissions)
components.add(fileStrings.sorted().join("|")) components.add(fileStrings.sorted().join("|"))
var userStrings: seq[string] = @[] var userStrings: seq[string] = @[]
@ -1926,7 +1948,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string =
var serviceStrings: seq[string] = @[] var serviceStrings: seq[string] = @[]
for service in manifest.services: for service in manifest.services:
serviceStrings.add("service:" & service.name & ":" & $service.enabled & ":" & service.content) serviceStrings.add("service:" & service.name & ":" & $service.enabled &
":" & service.content)
components.add(serviceStrings.sorted().join("|")) components.add(serviceStrings.sorted().join("|"))
# 14. Security / Sandbox # 14. Security / Sandbox
@ -1951,7 +1974,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string =
# 15. Desktop Integration # 15. Desktop Integration
if manifest.desktop.isSome: if manifest.desktop.isSome:
let dt = manifest.desktop.get() let dt = manifest.desktop.get()
var dtStr = "desktop:" & dt.displayName & ":" & $dt.terminal & ":" & $dt.startupNotify var dtStr = "desktop:" & dt.displayName & ":" & $dt.terminal & ":" &
$dt.startupNotify
if dt.icon.isSome: if dt.icon.isSome:
dtStr.add(":icon=" & dt.icon.get()) dtStr.add(":icon=" & dt.icon.get())
@ -1972,7 +1996,8 @@ proc calculateManifestHash*(manifest: PackageManifest): string =
return $hash return $hash
proc verifyManifestHash*(manifest: PackageManifest, expectedHash: string): bool = proc verifyManifestHash*(manifest: PackageManifest,
expectedHash: string): bool =
## Verify that a manifest matches the expected hash ## Verify that a manifest matches the expected hash
## Returns true if hash matches, false otherwise ## Returns true if hash matches, false otherwise
let calculatedHash = calculateManifestHash(manifest) let calculatedHash = calculateManifestHash(manifest)

View File

@ -29,7 +29,7 @@
import std/[os, strutils, times, options, sequtils, osproc, logging] import std/[os, strutils, times, options, sequtils, osproc, logging]
import nip/cas import nip/cas
import nip/xxhash import nip/xxh
import nip/nexter_manifest import nip/nexter_manifest
type type
@ -97,7 +97,8 @@ proc parseNEXTER*(path: string): NEXTERContainer =
try: try:
# Extract archive using tar with zstd decompression # Extract archive using tar with zstd decompression
# Using --auto-compress lets tar detect compression automatically # Using --auto-compress lets tar detect compression automatically
let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " & quoteShell(tempDir) let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " &
quoteShell(tempDir)
let exitCode = execCmd(extractCmd) let exitCode = execCmd(extractCmd)
if exitCode != 0: if exitCode != 0:
@ -208,7 +209,8 @@ proc createNEXTER*(manifest: NEXTERManifest, environment: string, chunks: seq[Ch
writeFile(tempDir / "signature.sig", signature) writeFile(tempDir / "signature.sig", signature)
# Create tar.zst archive # Create tar.zst archive
let createCmd = "tar --auto-compress -cf " & quoteShell(outputPath) & " -C " & quoteShell(tempDir) & " ." let createCmd = "tar --auto-compress -cf " & quoteShell(outputPath) &
" -C " & quoteShell(tempDir) & " ."
let exitCode = execCmd(createCmd) let exitCode = execCmd(createCmd)
if exitCode != 0: if exitCode != 0:
@ -247,7 +249,7 @@ proc extractChunksToCAS*(container: NEXTERContainer, casRoot: string): seq[strin
for chunk in container.chunks: for chunk in container.chunks:
try: try:
# Decompress chunk # Decompress chunk
let decompressed = chunk.data # TODO: Implement zstd decompression let decompressed = chunk.data # TODO: Implement zstd decompression
# Verify hash # Verify hash
let calculatedHash = "xxh3-" & $calculateXXH3(decompressed) let calculatedHash = "xxh3-" & $calculateXXH3(decompressed)

View File

@ -29,7 +29,7 @@
import std/[os, strutils, times, json, options, sequtils] import std/[os, strutils, times, json, options, sequtils]
import nip/cas import nip/cas
import nip/xxhash import nip/xxh
import nip/npk_manifest import nip/npk_manifest
import nip/manifest_parser import nip/manifest_parser
@ -97,7 +97,8 @@ proc parseNPK*(path: string): NPKPackage =
try: try:
# Extract archive using tar with zstd decompression # Extract archive using tar with zstd decompression
# Using --auto-compress lets tar detect compression automatically # Using --auto-compress lets tar detect compression automatically
let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " & quoteShell(tempDir) let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " &
quoteShell(tempDir)
let extractResult = execShellCmd(extractCmd) let extractResult = execShellCmd(extractCmd)
if extractResult != 0: if extractResult != 0:
@ -133,7 +134,7 @@ proc parseNPK*(path: string): NPKPackage =
hash: chunkHash, hash: chunkHash,
data: chunkData, data: chunkData,
size: chunkData.len.int64, size: chunkData.len.int64,
chunkType: Binary # Will be determined from manifest chunkType: Binary # Will be determined from manifest
)) ))
# Load signature # Load signature
@ -343,11 +344,13 @@ proc packageSize*(pkg: NPKPackage): int64 =
proc `$`*(pkg: NPKPackage): string = proc `$`*(pkg: NPKPackage): string =
## Convert NPK package to human-readable string ## Convert NPK package to human-readable string
result = "NPK Package: " & pkg.manifest.name & " v" & manifest_parser.`$`(pkg.manifest.version) & "\n" result = "NPK Package: " & pkg.manifest.name & " v" & manifest_parser.`$`(
pkg.manifest.version) & "\n"
result.add("Archive: " & pkg.archivePath & "\n") result.add("Archive: " & pkg.archivePath & "\n")
result.add("Chunks: " & $pkg.chunks.len & "\n") result.add("Chunks: " & $pkg.chunks.len & "\n")
result.add("Total Size: " & $(packageSize(pkg) div 1024) & " KB\n") result.add("Total Size: " & $(packageSize(pkg) div 1024) & " KB\n")
result.add("Signature: " & (if pkg.signature.len > 0: "Present" else: "Missing") & "\n") result.add("Signature: " & (if pkg.signature.len >
0: "Present" else: "Missing") & "\n")
# ============================================================================ # ============================================================================
# Error Formatting # Error Formatting

View File

@ -15,6 +15,7 @@ import std/[strutils]
# We'll use a conditional import to handle the case where xxhash isn't installed yet # We'll use a conditional import to handle the case where xxhash isn't installed yet
when defined(useXXHash): when defined(useXXHash):
import xxhash import xxhash
import nint128 # Required for UInt128 toHex
else: else:
# Fallback implementation using a simple hash for development # Fallback implementation using a simple hash for development
# This will be replaced with actual xxhash once the library is installed # This will be replaced with actual xxhash once the library is installed
@ -41,7 +42,8 @@ when defined(useXXHash):
proc calculateXXH3*(data: seq[byte]): XXH3Hash = proc calculateXXH3*(data: seq[byte]): XXH3Hash =
## Calculate xxh3-128 hash of a byte sequence ## Calculate xxh3-128 hash of a byte sequence
## Returns hash in format: "xxh3-<hex-digest>" ## Returns hash in format: "xxh3-<hex-digest>"
let hash128 = XXH3_128bits(cast[ptr UncheckedArray[byte]](unsafeAddr data[0]), data.len) let hash128 = XXH3_128bits(cast[ptr UncheckedArray[byte]](unsafeAddr data[
0]), csize_t(data.len))
let hexDigest = hash128.toHex().toLowerAscii() let hexDigest = hash128.toHex().toLowerAscii()
result = XXH3Hash("xxh3-" & hexDigest) result = XXH3Hash("xxh3-" & hexDigest)
@ -109,7 +111,7 @@ when isMainModule:
echo "✗ Hash verification failed" echo "✗ Hash verification failed"
# Test byte sequence hashing # Test byte sequence hashing
let testBytes = @[byte(72), byte(101), byte(108), byte(108), byte(111)] # "Hello" let testBytes = @[byte(72), byte(101), byte(108), byte(108), byte(111)] # "Hello"
let bytesHash = calculateXXH3(testBytes) let bytesHash = calculateXXH3(testBytes)
echo "Bytes hash: ", $bytesHash echo "Bytes hash: ", $bytesHash