fix: require exact package manifest names

This commit is contained in:
Hermes Agent
2026-09-03 20:28:18 +00:00
parent 532010b5dd
commit a9237346fa
6 changed files with 16 additions and 3 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog # Changelog
## v1.18.26-dev exact package manifest-name validation
- Added package-security coverage for a manifest name that only starts with the expected package name, such as `name: math-tools-extra` for package `math-tools`.
- Changed `claro package doctor` to compare the complete `name:` value, preventing prefix matches from being accepted as valid manifests.
## v1.18.26-dev package list unsafe-name diagnostic ## v1.18.26-dev package list unsafe-name diagnostic
- Added focused package-security validation for `claro package list` when `claro.project` already contains an unsafe package name such as `../bad`. - Added focused package-security validation for `claro package list` when `claro.project` already contains an unsafe package name such as `../bad`.
+1
View File
@@ -100,6 +100,7 @@ Ready now:
- project-name safety checks for `claro new`, so unsafe names such as `../bad` are rejected before Claro creates folders - project-name safety checks for `claro new`, so unsafe names such as `../bad` are rejected before Claro creates folders
- package-name safety checks when adding packages, including the 64-character package-name limit, when `claro package doctor` audits an existing `claro.project`, when `claro package lock` writes lockfile data, when `claro package list` shows existing packages, when `claro package init` sees unsafe names already present in `claro.project`, when `claro package add` sees unsafe names already present in `claro.project`, when `claro package remove` refreshes the lockfile after an edit, when learners need to remove an unsafe package entry that is already present, when `claro package doctor` verifies lockfile checksums for listed packages, and when `claro package doctor` rejects lockfile package entries that are not listed in `claro.project` - package-name safety checks when adding packages, including the 64-character package-name limit, when `claro package doctor` audits an existing `claro.project`, when `claro package lock` writes lockfile data, when `claro package list` shows existing packages, when `claro package init` sees unsafe names already present in `claro.project`, when `claro package add` sees unsafe names already present in `claro.project`, when `claro package remove` refreshes the lockfile after an edit, when learners need to remove an unsafe package entry that is already present, when `claro package doctor` verifies lockfile checksums for listed packages, and when `claro package doctor` rejects lockfile package entries that are not listed in `claro.project`
- `claro package doctor` rejects a listed package manifest whose `name:` does not match the package name in `claro.project` - `claro package doctor` rejects a listed package manifest whose `name:` does not match the package name in `claro.project`
- `claro package doctor` compares the complete manifest `name:` value, so a prefix such as `math-tools-extra` cannot be accepted for package `math-tools`
Still needed: Still needed:
- install from local path - install from local path
+1 -1
View File
@@ -33,7 +33,7 @@ See `CURRENT_STATUS.md` for the detailed feature matrix.
2. Keep examples aligned with the modern simple syntax (`END`, `DO`, short `SET`, short `ASK`) while documenting older compatibility forms separately. 2. Keep examples aligned with the modern simple syntax (`END`, `DO`, short `SET`, short `ASK`) while documenting older compatibility forms separately.
3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, package security validation, and CI workflow coverage validation). Current package safety validation covers safe project creation, rejecting unsafe project names during `claro new`, safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, refusing to write lockfile data for unsafe package names during `package lock`, making `package list` fail instead of displaying unsafe package entries as normal package names, making `package init` fail instead of reporting the project ready when unsafe package names are already present, making `package add` fail before changing files when unsafe package names are already present, making `package remove` fail instead of reporting success when unsafe package names remain during lockfile refresh, allowing `package remove` to remove an exact unsafe package entry so learners can repair a bad `claro.project`, making `package doctor` reject stale lockfile checksums for listed packages, and making `package doctor` reject lockfile package entries that are not listed in `claro.project`. Current function validation covers correct checked calls, wrong-type arguments, missing checked-argument diagnostics, missing unchecked-argument diagnostics for simple functions in both modern `DO greet` and empty compatibility `CALL greet WITH` forms, extra-argument diagnostics, and beginner-facing unknown-function diagnostics for mistyped modern `DO` and compatibility `CALL ... WITH` calls; current object-method parameter validation covers correct modern `DO object.method ...` and compatibility `CALL object.method WITH ...` checked calls, wrong-type diagnostics for both call forms, checked methods after another method in the same class, missing and extra checked-argument diagnostics including the extra-argument case for a checked method after another method in the same class, missing unchecked-argument diagnostics for simple object methods, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and class-specific unknown-method diagnostics for both modern and compatibility calls when a learner calls a method the class does not declare; current object-field validation covers direct NUMBER/TEXT/YESNO field-assignment positives, direct object-field `CHECK TYPE` metadata positives for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO field metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, direct wrong-type field assignment diagnostics, field collection when a `HAS` field appears after a simple method, direct unknown-field diagnostics for NUMBER/TEXT/YESNO values, a beginner-facing fallback for unknown fields assigned from expressions whose type is not inferable yet, and missing-object diagnostics for both `SET object.field value` and `CHECK TYPE object.field IS TYPE` before `NEW`. 3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, package security validation, and CI workflow coverage validation). Current package safety validation covers safe project creation, rejecting unsafe project names during `claro new`, safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, refusing to write lockfile data for unsafe package names during `package lock`, making `package list` fail instead of displaying unsafe package entries as normal package names, making `package init` fail instead of reporting the project ready when unsafe package names are already present, making `package add` fail before changing files when unsafe package names are already present, making `package remove` fail instead of reporting success when unsafe package names remain during lockfile refresh, allowing `package remove` to remove an exact unsafe package entry so learners can repair a bad `claro.project`, making `package doctor` reject stale lockfile checksums for listed packages, and making `package doctor` reject lockfile package entries that are not listed in `claro.project`. Current function validation covers correct checked calls, wrong-type arguments, missing checked-argument diagnostics, missing unchecked-argument diagnostics for simple functions in both modern `DO greet` and empty compatibility `CALL greet WITH` forms, extra-argument diagnostics, and beginner-facing unknown-function diagnostics for mistyped modern `DO` and compatibility `CALL ... WITH` calls; current object-method parameter validation covers correct modern `DO object.method ...` and compatibility `CALL object.method WITH ...` checked calls, wrong-type diagnostics for both call forms, checked methods after another method in the same class, missing and extra checked-argument diagnostics including the extra-argument case for a checked method after another method in the same class, missing unchecked-argument diagnostics for simple object methods, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and class-specific unknown-method diagnostics for both modern and compatibility calls when a learner calls a method the class does not declare; current object-field validation covers direct NUMBER/TEXT/YESNO field-assignment positives, direct object-field `CHECK TYPE` metadata positives for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO field metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, direct wrong-type field assignment diagnostics, field collection when a `HAS` field appears after a simple method, direct unknown-field diagnostics for NUMBER/TEXT/YESNO values, a beginner-facing fallback for unknown fields assigned from expressions whose type is not inferable yet, and missing-object diagnostics for both `SET object.field value` and `CHECK TYPE object.field IS TYPE` before `NEW`.
3a. Keep package validation honest by checking that each listed manifest declares the expected package name. 3a. Keep package validation honest by checking that each listed manifest declares the expected package name.
3b. Keep package security validation in Forgejo/Gitea CI so unsafe names, manifest mismatches, missing manifests, and lockfile errors remain release blockers. 3b. Keep package security validation in Forgejo/Gitea CI so unsafe names, exact manifest-name mismatches, missing manifests, and lockfile errors remain release blockers.
4. Add small examples for each foundation feature before adding bigger syntax. 4. Add small examples for each foundation feature before adding bigger syntax.
## Complete-platform milestones ## Complete-platform milestones
+1 -1
View File
@@ -63,7 +63,7 @@ source: local
checksum: 1234abcd checksum: 1234abcd
``` ```
`claro.lock` records the release version, lock format, packages, and checksums so future registry work has a stable safety foundation. `claro package doctor` checks those lockfile checksums for listed packages and reports `BAD lock checksum: name` if the lockfile is stale or edited incorrectly. It also reports `BAD lock package not in claro.project: name` when the lockfile contains a package entry that is not listed in `claro.project`, so learners know to refresh the lockfile instead of trusting stale package data. `claro.lock` records the release version, lock format, packages, and checksums so future registry work has a stable safety foundation. `claro package doctor` checks those lockfile checksums for listed packages and reports `BAD lock checksum: name` if the lockfile is stale or edited incorrectly. It also reports `BAD lock package not in claro.project: name` when the lockfile contains a package entry that is not listed in `claro.project`, so learners know to refresh the lockfile instead of trusting stale package data. Package manifests must declare the complete expected `name:` value; a name that only starts with the package name is rejected as `BAD package manifest name: name`.
## Project-name safety ## Project-name safety
+2 -1
View File
@@ -638,7 +638,8 @@ static void create_package_folder(const char *name){ char path[512], meta[768],
static int list_project_packages(void){ char *txt=read_file_text("claro.project"); char *p; int count=0; printf("Packages in claro.project:\n"); if(!txt){ printf(" (no claro.project yet)\n"); return 0; } if(!project_package_names_safe()){ free(txt); return 1; } p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ printf(" %s\n",pkg); count++; } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } if(!count) printf(" (none)\n"); free(txt); return 0; } static int list_project_packages(void){ char *txt=read_file_text("claro.project"); char *p; int count=0; printf("Packages in claro.project:\n"); if(!txt){ printf(" (no claro.project yet)\n"); return 0; } if(!project_package_names_safe()){ free(txt); return 1; } p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ printf(" %s\n",pkg); count++; } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } if(!count) printf(" (none)\n"); free(txt); return 0; }
static int package_lock_checksum_ok(const char *name){ char *txt=read_file_text("claro.lock"); char *p; int in_pkg=0, ok=0; char expected[32]; if(!txt) return 0; package_checksum(name,expected,sizeof(expected)); p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); in_pkg=(strcmp(pkg,name)==0); } else if(in_pkg&&strnicmp2(t,"checksum:",9)==0){ char *sum=trim_inplace(t+9); ok=(strcmp(sum,expected)==0); break; } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } free(txt); return ok; } static int package_lock_checksum_ok(const char *name){ char *txt=read_file_text("claro.lock"); char *p; int in_pkg=0, ok=0; char expected[32]; if(!txt) return 0; package_checksum(name,expected,sizeof(expected)); p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); in_pkg=(strcmp(pkg,name)==0); } else if(in_pkg&&strnicmp2(t,"checksum:",9)==0){ char *sum=trim_inplace(t+9); ok=(strcmp(sum,expected)==0); break; } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } free(txt); return ok; }
static int lock_packages_match_project(void){ char *txt=read_file_text("claro.lock"); char *p; int ok=1; if(!txt) return 1; p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg&&!package_name_safe(pkg)){ printf(" BAD lock package name: %s\n",pkg); ok=0; } else if(*pkg&&!package_has_name(pkg)){ printf(" BAD lock package not in claro.project: %s\n",pkg); ok=0; } } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } free(txt); return ok; } static int lock_packages_match_project(void){ char *txt=read_file_text("claro.lock"); char *p; int ok=1; if(!txt) return 1; p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg&&!package_name_safe(pkg)){ printf(" BAD lock package name: %s\n",pkg); ok=0; } else if(*pkg&&!package_has_name(pkg)){ printf(" BAD lock package not in claro.project: %s\n",pkg); ok=0; } } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } free(txt); return ok; }
static int package_doctor(void){ int ok=1; char *mainfile; char *txt; printf("%s\n",CLARO_VERSION); printf("Package/project doctor:\n"); printf(" %s claro.project\n",file_exists_simple("claro.project")?"OK":"MISSING"); if(!file_exists_simple("claro.project")) ok=0; mainfile=project_value("main"); if(mainfile&&*mainfile){ printf(" %s main script: %s\n",file_exists_simple(mainfile)?"OK":"MISSING",mainfile); if(!file_exists_simple(mainfile)) ok=0; } else { printf(" MISSING main setting\n"); ok=0; } printf(" %s packages folder\n",path_exists_simple("packages")?"OK":"MISSING"); if(!path_exists_simple("packages")) ok=0; printf(" %s claro.lock\n",file_exists_simple("claro.lock")?"OK":"MISSING"); if(!file_exists_simple("claro.lock")) ok=0; txt=read_file_text("claro.project"); if(txt){ char *p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ char meta[512], *mt, sum[32]; if(!package_name_safe(pkg)){ printf(" BAD package name: %s\n",pkg); ok=0; } else { snprintf(meta,sizeof(meta),"packages/%s/claro.package",pkg); mt=read_file_text(meta); if(mt&&strstr(mt,"manifest-version: 1")&&strstr(mt,"checksum:")){ char expected_name[128]; snprintf(expected_name,sizeof(expected_name),"name: %s",pkg); if(!strstr(mt,expected_name)){ printf(" BAD package manifest name: %s\n",pkg); ok=0; } else { printf(" OK package manifest: %s\n",pkg); package_checksum(pkg,sum,sizeof(sum)); if(strstr(mt,sum)) printf(" OK package checksum: %s\n",pkg); else { printf(" BAD package checksum: %s\n",pkg); ok=0; } if(package_lock_checksum_ok(pkg)) printf(" OK lock checksum: %s\n",pkg); else { printf(" BAD lock checksum: %s\n",pkg); ok=0; } } } else { printf(" MISSING package manifest: %s\n",pkg); ok=0; } free(mt); } } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); } if(!lock_packages_match_project()) ok=0; free(mainfile); printf("%s\n",ok?"Package/project files look ready.":"Package/project files need attention."); return ok?0:1; } static int package_manifest_name_matches(const char *text,const char *name){ const char *p=text; size_t n=strlen(name); while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=5&&strncmp(line,"name:",5)==0){ const char *value=line+5; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,name,n)==0) return 1; } while(*p=='\n'||*p=='\r') p++; } return 0; }
static int package_doctor(void){ int ok=1; char *mainfile; char *txt; printf("%s\n",CLARO_VERSION); printf("Package/project doctor:\n"); printf(" %s claro.project\n",file_exists_simple("claro.project")?"OK":"MISSING"); if(!file_exists_simple("claro.project")) ok=0; mainfile=project_value("main"); if(mainfile&&*mainfile){ printf(" %s main script: %s\n",file_exists_simple(mainfile)?"OK":"MISSING",mainfile); if(!file_exists_simple(mainfile)) ok=0; } else { printf(" MISSING main setting\n"); ok=0; } printf(" %s packages folder\n",path_exists_simple("packages")?"OK":"MISSING"); if(!path_exists_simple("packages")) ok=0; printf(" %s claro.lock\n",file_exists_simple("claro.lock")?"OK":"MISSING"); if(!file_exists_simple("claro.lock")) ok=0; txt=read_file_text("claro.project"); if(txt){ char *p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ char meta[512], *mt, sum[32]; if(!package_name_safe(pkg)){ printf(" BAD package name: %s\n",pkg); ok=0; } else { snprintf(meta,sizeof(meta),"packages/%s/claro.package",pkg); mt=read_file_text(meta); if(mt&&strstr(mt,"manifest-version: 1")&&strstr(mt,"checksum:")){ if(!package_manifest_name_matches(mt,pkg)){ printf(" BAD package manifest name: %s\n",pkg); ok=0; } else { printf(" OK package manifest: %s\n",pkg); package_checksum(pkg,sum,sizeof(sum)); if(strstr(mt,sum)) printf(" OK package checksum: %s\n",pkg); else { printf(" BAD package checksum: %s\n",pkg); ok=0; } if(package_lock_checksum_ok(pkg)) printf(" OK lock checksum: %s\n",pkg); else { printf(" BAD lock checksum: %s\n",pkg); ok=0; } } } else { printf(" MISSING package manifest: %s\n",pkg); ok=0; } free(mt); } } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); } if(!lock_packages_match_project()) ok=0; free(mainfile); printf("%s\n",ok?"Package/project files look ready.":"Package/project files need attention."); return ok?0:1; }
static int run_package_cmd(int argc,char **argv,int arg){ if(arg>=argc){ printf("Package commands:\n claro package init\n claro package add NAME\n claro package remove NAME\n claro package list\n claro package doctor\n claro package lock\n"); return 0; } if(strcmp(argv[arg],"init")==0){ ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); if(!write_package_lock()) return 1; printf("Package project ready: claro.project, claro.lock, packages/\n"); return 0; } if(strcmp(argv[arg],"add")==0&&arg+1<argc){ const char *name=argv[arg+1]; int already; if(package_name_too_long(name)){ fprintf(stderr,"Package names must be 64 characters or fewer.\n"); return 1; } if(!package_name_safe(name)){ fprintf(stderr,"Package names may use only letters, numbers, dash, and underscore.\n"); return 1; } ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); already=package_has_name(name); if(!append_package_to_project(name)) return 1; create_package_folder(name); if(!write_package_lock()) return 1; if(!already) printf("Added package: %s\n",name); return 0; } if(strcmp(argv[arg],"remove")==0&&arg+1<argc){ const char *name=argv[arg+1]; int removed; removed=remove_package_from_project(name); if(removed<0) return 1; if(removed) printf("Removed package from project: %s\n",name); else printf("Package was not listed: %s\n",name); return 0; } if(strcmp(argv[arg],"list")==0) return list_project_packages(); if(strcmp(argv[arg],"doctor")==0) return package_doctor(); if(strcmp(argv[arg],"lock")==0||strcmp(argv[arg],"update")==0){ ensure_project_file(); make_folder("packages"); if(write_package_lock()){ printf("Updated claro.lock\n"); return 0; } fprintf(stderr,"Could not write claro.lock\n"); return 1; } printf("Unknown package command. Try: claro package init\n"); return 1; } static int run_package_cmd(int argc,char **argv,int arg){ if(arg>=argc){ printf("Package commands:\n claro package init\n claro package add NAME\n claro package remove NAME\n claro package list\n claro package doctor\n claro package lock\n"); return 0; } if(strcmp(argv[arg],"init")==0){ ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); if(!write_package_lock()) return 1; printf("Package project ready: claro.project, claro.lock, packages/\n"); return 0; } if(strcmp(argv[arg],"add")==0&&arg+1<argc){ const char *name=argv[arg+1]; int already; if(package_name_too_long(name)){ fprintf(stderr,"Package names must be 64 characters or fewer.\n"); return 1; } if(!package_name_safe(name)){ fprintf(stderr,"Package names may use only letters, numbers, dash, and underscore.\n"); return 1; } ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); already=package_has_name(name); if(!append_package_to_project(name)) return 1; create_package_folder(name); if(!write_package_lock()) return 1; if(!already) printf("Added package: %s\n",name); return 0; } if(strcmp(argv[arg],"remove")==0&&arg+1<argc){ const char *name=argv[arg+1]; int removed; removed=remove_package_from_project(name); if(removed<0) return 1; if(removed) printf("Removed package from project: %s\n",name); else printf("Package was not listed: %s\n",name); return 0; } if(strcmp(argv[arg],"list")==0) return list_project_packages(); if(strcmp(argv[arg],"doctor")==0) return package_doctor(); if(strcmp(argv[arg],"lock")==0||strcmp(argv[arg],"update")==0){ ensure_project_file(); make_folder("packages"); if(write_package_lock()){ printf("Updated claro.lock\n"); return 0; } fprintf(stderr,"Could not write claro.lock\n"); return 1; } printf("Unknown package command. Try: claro package init\n"); return 1; }
static int print_ide_info(void){ static int print_ide_info(void){
printf("{\n"); printf("{\n");
+6
View File
@@ -123,6 +123,12 @@ def main():
fail("package doctor must reject a manifest whose name does not match the project package") fail("package doctor must reject a manifest whose name does not match the project package")
if "BAD package manifest name: math-tools" not in out: if "BAD package manifest name: math-tools" not in out:
fail(f"package doctor manifest-name diagnostic was unclear:\n{out}") fail(f"package doctor manifest-name diagnostic was unclear:\n{out}")
manifest_path.write_text(manifest.replace("name: math-tools", "name: math-tools-extra"), encoding="utf-8")
rc, out = run([str(EXE), "package", "doctor"], work)
if rc == 0:
fail("package doctor must reject a manifest name that only starts with the project package name")
if "BAD package manifest name: math-tools" not in out:
fail(f"Package manifest prefix-match diagnostic was unclear:\n{out}")
manifest_path.write_text(manifest, encoding="utf-8") manifest_path.write_text(manifest, encoding="utf-8")
(work / "claro.lock").write_text( (work / "claro.lock").write_text(