-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathvalidate-agent-policy.py
More file actions
1260 lines (1141 loc) · 43 KB
/
Copy pathvalidate-agent-policy.py
File metadata and controls
1260 lines (1141 loc) · 43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
import argparse
import json
import os
import re
import stat
from pathlib import Path
from typing import cast
ALLOWED_KINDS = {
"shared-policy",
"scoped-guidance",
"runbook",
"decision",
"agent",
"skill",
"adapter",
"enforcement",
}
ALLOWED_AUTHORITIES = {
"canonical",
"canonical-detail",
"adapter-only",
"advisory",
}
ALLOWED_CONSUMERS = {
"codex",
"claude-code",
"copilot",
"gemini-cli",
"human",
"ci",
}
COPILOT_ADAPTER = "@../AGENTS.md\n"
FORBIDDEN_PUBLIC_TOKENS = (
"repos/org/z-shell-dot-github",
"workspace/repos.yml",
"root meta-workspace",
"meta-workspace root",
"private meta-workspace",
"memory/",
"ZSHELL_MEMORY_GIST_ID",
"scripts/memory-sync.sh",
"user-profile.md",
"/mnt/workspace",
"~/Codespace",
".gitmodules",
)
FORBIDDEN_PUBLIC_PATH = re.compile(
r"(?<![A-Za-z0-9_{])repos/" r"(?:annexes|core|docs|env|org|packages|plugins|tools)/"
)
MANIFEST_PATH = ".github/instruction-surfaces.json"
PUBLIC_POLICY_BYTE_LIMIT = 32_768
EXPECTED_REPOSITORY = "z-shell/.github"
REQUIRED_SURFACE_FIELDS = (
"id",
"path",
"kind",
"authority",
"consumers",
"tasks",
"file_patterns",
"required",
"review_owner",
"canonical_for",
)
BASE_INVENTORY = {
"AGENTS.md": "shared-policy",
"PATTERNS.md": "shared-policy",
".github/AGENT_MEMORY.md": "runbook",
".github/README.md": "runbook",
".github/copilot-instructions.md": "adapter",
}
INVENTORY_RULES = (
(".github/instructions", ".instructions.md", "scoped-guidance", True),
(".github/agents", ".md", "agent", False),
("runbooks", ".md", "runbook", False),
("decisions", ".md", "decision", False),
)
ENFORCEMENT_INVENTORY = {
".github/workflows/agent-instructions.yml": "enforcement",
"scripts/validate-agent-policy.py": "enforcement",
}
PUBLIC_SCAN_EXEMPTIONS = {"scripts/validate-agent-policy.py"}
def error(path: str, rule: str, fix: str) -> str:
return f"{path}: {rule}; fix: {fix}"
def _invalid_single_line_character(character: str) -> bool:
codepoint = ord(character)
return codepoint < 32 or 127 <= codepoint <= 159 or codepoint in {0x2028, 0x2029}
def _one_line_path(path: str) -> str:
return "".join(
(
"\\\\"
if character == "\\"
else (
f"\\x{ord(character):02x}"
if _invalid_single_line_character(character)
else character
)
)
for character in path
)
def _invalid_discovered_path_error(path: str) -> str:
display_path = _one_line_path(path)
return error(
display_path,
"discovered path cannot be represented safely in one-line diagnostics",
f"rename or remove the invalid path {display_path}",
)
def _validated_manifest_path(root: Path) -> tuple[Path | None, list[str]]:
manifest_path = root / MANIFEST_PATH
try:
file_status = manifest_path.lstat()
except OSError as exc:
return None, [
error(
MANIFEST_PATH,
f"manifest must exist as a contained regular file: {exc}",
f"restore a regular file at {MANIFEST_PATH}",
)
]
if stat.S_ISLNK(file_status.st_mode):
return None, [
error(
MANIFEST_PATH,
"manifest must be a contained regular file, not a symlink",
f"replace {MANIFEST_PATH} with a regular file inside the repository",
)
]
if not stat.S_ISREG(file_status.st_mode):
return None, [
error(
MANIFEST_PATH,
"manifest must be a contained regular file",
f"replace {MANIFEST_PATH} with a regular file inside the repository",
)
]
try:
resolved = manifest_path.resolve(strict=True)
except (OSError, RuntimeError, ValueError) as exc:
return None, [
error(
MANIFEST_PATH,
f"cannot resolve manifest path: {exc}",
f"restore a regular file at {MANIFEST_PATH}",
)
]
if not resolved.is_relative_to(root):
return None, [
error(
MANIFEST_PATH,
"manifest path escapes repository",
f"move {MANIFEST_PATH} inside the repository",
)
]
return resolved, []
def _reject_duplicate_json_keys(
pairs: list[tuple[str, object]],
) -> dict[str, object]:
parsed: dict[str, object] = {}
for key, value in pairs:
if key in parsed:
raise ValueError(f"duplicate JSON key {key!r}")
parsed[key] = value
return parsed
def _load_manifest(manifest_path: Path) -> dict[str, object]:
with manifest_path.open(encoding="utf-8") as manifest_file:
try:
return cast(
dict[str, object],
json.load(
manifest_file,
object_pairs_hook=_reject_duplicate_json_keys,
),
)
except json.JSONDecodeError:
raise
except (ValueError, RecursionError) as exc:
raise json.JSONDecodeError(str(exc), "", 0) from exc
def _declared_surfaces(manifest: object) -> list[dict[str, object]]:
if not isinstance(manifest, dict):
return []
surfaces = manifest.get("surfaces")
if not isinstance(surfaces, list):
return []
return [surface for surface in surfaces if isinstance(surface, dict)]
def _non_empty_string(value: object) -> bool:
return (
isinstance(value, str)
and bool(value.strip())
and not any(_invalid_single_line_character(character) for character in value)
)
def _string_list(value: object, *, allow_empty: bool = False) -> bool:
return (
isinstance(value, list)
and (allow_empty or bool(value))
and all(_non_empty_string(item) for item in value)
)
def _surface_name(surface: dict[str, object], index: int) -> str:
surface_id = surface.get("id")
if _non_empty_string(surface_id):
return cast(str, surface_id)
return f"surfaces[{index}]"
def _surface_path(surface: dict[str, object]) -> str | None:
path = surface.get("path")
if _non_empty_string(path):
return cast(str, path)
return None
def _inventory_path(relative_path: str) -> str:
return Path(os.path.normpath(relative_path)).as_posix()
def _expected_inventory_kind(relative_path: str) -> str | None:
inventory_path = _inventory_path(relative_path)
exact_kind = BASE_INVENTORY.get(inventory_path) or ENFORCEMENT_INVENTORY.get(
inventory_path
)
if exact_kind is not None:
return exact_kind
path = Path(inventory_path)
for relative_directory, suffix, kind, recursive in INVENTORY_RULES:
within_directory = inventory_path.startswith(f"{relative_directory}/")
if (
path.name.endswith(suffix)
and within_directory
and (recursive or path.parent.as_posix() == relative_directory)
):
return kind
if re.fullmatch(r"\.github/skills/[^/]+/SKILL\.md", inventory_path):
return "skill"
return None
def _resolve_declared_path(root: Path, relative_path: str) -> Path | None:
try:
resolved = (root / relative_path).resolve(strict=False)
except (OSError, RuntimeError, ValueError):
return None
if not resolved.is_relative_to(root.resolve()):
return None
return resolved
def _read_utf8(path: Path, display_path: str) -> tuple[str | None, list[str]]:
try:
return path.read_text(encoding="utf-8"), []
except UnicodeError as exc:
return None, [
error(
display_path,
f"invalid UTF-8 text: {exc}",
f"rewrite {display_path} as valid UTF-8",
)
]
except OSError as exc:
return None, [
error(
display_path,
f"cannot read declared surface: {exc}",
f"restore a readable regular file at {display_path}",
)
]
def _inventory_scan_error(relative_path: str, exc: OSError) -> str:
display_path = _one_line_path(relative_path)
if display_path != relative_path:
return _invalid_discovered_path_error(relative_path)
return error(
display_path,
f"cannot scan inventory directory: {exc}",
f"restore readable directory permissions for {display_path}",
)
def _validated_inventory_directory(
root: Path, relative_directory: str
) -> tuple[Path | None, list[str]]:
directory = root / relative_directory
try:
if not os.path.lexists(directory):
return None, []
directory_status = directory.lstat()
except OSError as exc:
return None, [_inventory_scan_error(relative_directory, exc)]
if stat.S_ISLNK(directory_status.st_mode):
return None, [
error(
relative_directory,
"inventory directory symlink is not allowed",
f"replace {relative_directory} with a regular directory "
"inside the repository",
)
]
return directory, []
def _scan_flat_inventory(
root: Path, relative_directory: str, suffix: str, recursive: bool
) -> tuple[set[str], list[str]]:
directory, errors = _validated_inventory_directory(root, relative_directory)
if directory is None:
return set(), errors
paths: set[str] = set()
pending = [(directory, relative_directory)]
while pending:
current_directory, current_relative_directory = pending.pop()
try:
entries_context = os.scandir(current_directory)
except OSError as exc:
errors.append(_inventory_scan_error(current_relative_directory, exc))
continue
try:
with entries_context as entries:
for entry in entries:
relative_path = f"{current_relative_directory}/{entry.name}"
if entry.name.endswith(suffix):
paths.add(relative_path)
if not recursive:
continue
try:
if entry.is_dir(follow_symlinks=False):
pending.append((Path(entry.path), relative_path))
except OSError as exc:
errors.append(_inventory_scan_error(relative_path, exc))
except OSError as exc:
errors.append(_inventory_scan_error(current_relative_directory, exc))
return paths, errors
def _skill_resource_symlink_error(relative_path: str) -> str:
return error(
relative_path,
"skill resource symlink is not allowed (file or directory symlink)",
f"replace {relative_path} with a regular file or regular directory "
"below the repository root",
)
def _scan_skill_inventory(root: Path) -> tuple[set[str], list[str]]:
relative_directory = ".github/skills"
directory, errors = _validated_inventory_directory(root, relative_directory)
if directory is None:
return set(), errors
paths: set[str] = set()
try:
with os.scandir(directory) as skill_entries:
for skill_entry in skill_entries:
skill_path = f"{relative_directory}/{skill_entry.name}"
try:
if skill_entry.is_symlink():
errors.append(_skill_resource_symlink_error(skill_path))
continue
is_directory = skill_entry.is_dir(follow_symlinks=False)
except OSError as exc:
errors.append(_inventory_scan_error(skill_path, exc))
continue
if not is_directory:
continue
skill_directory = skill_path
try:
with os.scandir(root / skill_directory) as files:
if any(entry.name == "SKILL.md" for entry in files):
paths.add(f"{skill_directory}/SKILL.md")
except OSError as exc:
errors.append(_inventory_scan_error(skill_directory, exc))
except OSError as exc:
errors.append(_inventory_scan_error(relative_directory, exc))
return paths, errors
def _required_inventory(root: Path) -> tuple[set[str], list[str]]:
paths = set(BASE_INVENTORY)
errors: list[str] = []
for relative_directory, suffix, _kind, recursive in INVENTORY_RULES:
discovered, scan_errors = _scan_flat_inventory(
root,
relative_directory,
suffix,
recursive,
)
paths.update(discovered)
errors.extend(scan_errors)
discovered_skills, skill_errors = _scan_skill_inventory(root)
paths.update(discovered_skills)
errors.extend(skill_errors)
paths.update(
relative_path
for relative_path in ENFORCEMENT_INVENTORY
if os.path.lexists(root / relative_path)
)
return paths, errors
def validate_manifest(root: Path, manifest: dict[str, object]) -> list[str]:
errors: list[str] = []
root = root.resolve()
if not isinstance(manifest, dict):
return [
error(
MANIFEST_PATH,
"top-level JSON value must be an object",
f"edit {MANIFEST_PATH} so its top-level value is an object",
)
]
if type(manifest.get("version")) is not int or manifest.get("version") != 1:
errors.append(
error(
MANIFEST_PATH,
f"unsupported version {manifest.get('version')!r}; expected version 1",
f"set version to 1 in {MANIFEST_PATH}",
)
)
if manifest.get("repository") != EXPECTED_REPOSITORY:
errors.append(
error(
MANIFEST_PATH,
f"repository must be exactly {EXPECTED_REPOSITORY!r}",
f"set repository to {EXPECTED_REPOSITORY!r} in {MANIFEST_PATH}",
)
)
surfaces_value = manifest.get("surfaces")
if not isinstance(surfaces_value, list):
errors.append(
error(
MANIFEST_PATH,
"surfaces must be a list",
f"set surfaces to a JSON list in {MANIFEST_PATH}",
)
)
surfaces_value = []
seen_ids: dict[str, int] = {}
seen_paths: dict[Path, str] = {}
seen_canonical_domains: dict[str, str] = {}
declared_inventory: set[str] = set()
for index, surface_value in enumerate(surfaces_value):
if not isinstance(surface_value, dict):
errors.append(
error(
MANIFEST_PATH,
f"surfaces[{index}] must be an object",
f"replace surfaces[{index}] with a surface object in {MANIFEST_PATH}",
)
)
continue
surface = surface_value
name = _surface_name(surface, index)
for field in REQUIRED_SURFACE_FIELDS:
if field not in surface:
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} is missing required field {field!r}",
f"add {field} to surface {name!r} in {MANIFEST_PATH}",
)
)
for field in ("id", "path", "review_owner"):
if field in surface and not _non_empty_string(surface.get(field)):
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} field {field!r} must be a non-empty string",
f"set {field} to a non-empty string for surface {name!r}",
)
)
kind = surface.get("kind")
if not isinstance(kind, str) or kind not in ALLOWED_KINDS:
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} has unknown kind {kind!r}",
f"set kind for surface {name!r} to one of {sorted(ALLOWED_KINDS)!r}",
)
)
authority = surface.get("authority")
if not isinstance(authority, str) or authority not in ALLOWED_AUTHORITIES:
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} has unknown authority {authority!r}",
"set authority for surface "
f"{name!r} to one of {sorted(ALLOWED_AUTHORITIES)!r}",
)
)
for field in ("consumers", "tasks", "file_patterns"):
if field in surface and not _string_list(surface.get(field)):
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} field {field!r} must be a non-empty list "
"of non-empty strings",
f"set {field} to a non-empty string list for surface {name!r}",
)
)
canonical_for = surface.get("canonical_for")
if "canonical_for" in surface and not _string_list(
canonical_for, allow_empty=True
):
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} field 'canonical_for' must be a list of "
"non-empty strings",
f"set canonical_for to a string list for surface {name!r}",
)
)
required = surface.get("required")
if "required" in surface and type(required) is not bool:
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} field 'required' must be a Boolean",
f"set required to true or false for surface {name!r}",
)
)
consumers = surface.get("consumers")
if isinstance(consumers, list):
for consumer in consumers:
if isinstance(consumer, str) and consumer not in ALLOWED_CONSUMERS:
errors.append(
error(
MANIFEST_PATH,
f"surface {name!r} has unknown consumer {consumer!r}",
"set consumers for surface "
f"{name!r} to values from {sorted(ALLOWED_CONSUMERS)!r}",
)
)
surface_id = surface.get("id")
if _non_empty_string(surface_id):
surface_id = cast(str, surface_id)
if surface_id in seen_ids:
errors.append(
error(
MANIFEST_PATH,
f"duplicate surface id {surface_id!r}",
f"give every surface in {MANIFEST_PATH} a unique id",
)
)
else:
seen_ids[surface_id] = index
if isinstance(canonical_for, list):
for domain in canonical_for:
if not _non_empty_string(domain):
continue
domain = cast(str, domain)
if domain in seen_canonical_domains:
errors.append(
error(
MANIFEST_PATH,
f"duplicate canonical owner for domain {domain!r}",
f"leave exactly one canonical_for owner for {domain!r}",
)
)
else:
seen_canonical_domains[domain] = name
if required is not True:
errors.append(
error(
MANIFEST_PATH,
f"canonical owner for {domain!r} must be required",
f"set required to true for surface {name!r}",
)
)
if isinstance(kind, str) and kind in {"agent", "skill"}:
errors.append(
error(
MANIFEST_PATH,
f"{kind} surface {name!r} cannot own canonical_for "
f"domain {domain!r}",
f"move {domain!r} ownership to a policy surface and set "
f"canonical_for to [] for {name!r}",
)
)
if not isinstance(authority, str) or authority not in {
"canonical",
"canonical-detail",
}:
errors.append(
error(
MANIFEST_PATH,
f"canonical owner for {domain!r} must use canonical or "
"canonical-detail authority",
f"set a canonical authority for surface {name!r}",
)
)
if kind == "adapter" and (authority != "adapter-only" or canonical_for != []):
errors.append(
error(
MANIFEST_PATH,
f"adapter surface {name!r} cannot be canonical and must use "
"adapter-only authority with an empty canonical_for list",
f"set authority to adapter-only and canonical_for to [] for {name!r}",
)
)
relative_path = _surface_path(surface)
if relative_path is None:
continue
try:
resolved = (root / relative_path).resolve(strict=False)
except (OSError, RuntimeError, ValueError) as exc:
errors.append(
error(
relative_path,
f"cannot resolve declared path: {exc}",
f"replace {relative_path!r} with a valid repository-relative path",
)
)
continue
if not resolved.is_relative_to(root):
errors.append(
error(
relative_path,
"declared path escapes repository",
f"replace {relative_path!r} with a path below the repository root",
)
)
continue
inventory_path = _inventory_path(relative_path)
declared_inventory.add(inventory_path)
expected_kind = _expected_inventory_kind(relative_path)
if expected_kind is not None and kind != expected_kind:
errors.append(
error(
relative_path,
f"declared kind {kind!r} does not match inventory kind "
f"{expected_kind!r}",
f"set kind to {expected_kind!r} for surface {name!r}",
)
)
if resolved in seen_paths:
errors.append(
error(
relative_path,
f"duplicate declared path also used by {seen_paths[resolved]!r}",
f"give every surface in {MANIFEST_PATH} a unique path",
)
)
else:
seen_paths[resolved] = name
try:
is_regular_file = resolved.is_file()
except OSError as exc:
errors.append(
error(
relative_path,
f"cannot inspect declared path: {exc}",
f"restore a readable regular file at {relative_path}",
)
)
else:
if not is_regular_file:
errors.append(
error(
relative_path,
"declared path must exist as a regular file",
f"create the regular file {relative_path} or remove its manifest entry",
)
)
required_inventory, inventory_errors = _required_inventory(root)
errors.extend(inventory_errors)
for missing_path in sorted(required_inventory - declared_inventory):
display_path = _one_line_path(missing_path)
if display_path != missing_path:
errors.append(_invalid_discovered_path_error(missing_path))
continue
errors.append(
error(
display_path,
"surface is missing from manifest inventory",
f"declare {display_path} in {MANIFEST_PATH}",
)
)
canonical_policy = manifest.get("canonical_policy")
if canonical_policy != "AGENTS.md":
errors.append(
error(
MANIFEST_PATH,
"canonical_policy must be exactly 'AGENTS.md'",
f"set canonical_policy to 'AGENTS.md' in {MANIFEST_PATH}",
)
)
canonical_surface: dict[str, object] | None = None
if _non_empty_string(canonical_policy):
canonical_path = _resolve_declared_path(root, cast(str, canonical_policy))
if canonical_path is not None:
for surface in _declared_surfaces(manifest):
relative_path = _surface_path(surface)
if relative_path is None:
continue
if _resolve_declared_path(root, relative_path) == canonical_path:
canonical_surface = surface
break
if canonical_surface is None or not (
canonical_surface.get("kind") == "shared-policy"
and canonical_surface.get("authority") == "canonical"
and canonical_surface.get("required") is True
and isinstance(canonical_surface.get("canonical_for"), list)
and "organization-policy" in canonical_surface.get("canonical_for", [])
):
errors.append(
error(
MANIFEST_PATH,
"canonical_policy must resolve to the required canonical shared-policy "
"surface that owns 'organization-policy'",
f"set canonical_policy to the organization-policy path in {MANIFEST_PATH}",
)
)
return errors
def _walk_skill_resources(
root: Path, skill_directory: Path
) -> tuple[dict[str, Path], list[str]]:
files: dict[str, Path] = {}
errors: list[str] = []
directories = [skill_directory]
while directories:
directory = directories.pop()
raw_display_directory = directory.relative_to(root).as_posix()
display_directory = _one_line_path(raw_display_directory)
if display_directory != raw_display_directory:
errors.append(_invalid_discovered_path_error(raw_display_directory))
continue
try:
with os.scandir(directory) as entries:
for entry in entries:
path = Path(entry.path)
raw_display_path = path.relative_to(root).as_posix()
display_path = _one_line_path(raw_display_path)
if display_path != raw_display_path:
errors.append(_invalid_discovered_path_error(raw_display_path))
continue
try:
if entry.is_symlink():
errors.append(_skill_resource_symlink_error(display_path))
continue
if entry.is_file(follow_symlinks=False):
resolved = path.resolve(strict=False)
if not resolved.is_relative_to(root):
errors.append(
error(
display_path,
"skill resource escapes repository",
f"replace {display_path} with a regular file below "
"the repository root",
)
)
else:
files[display_path] = resolved
elif entry.is_dir(follow_symlinks=False):
directories.append(path)
except (OSError, RuntimeError, ValueError) as exc:
errors.append(
error(
display_path,
f"cannot inspect skill resource: {exc}",
f"repair or remove unreadable skill resource {display_path}",
)
)
except OSError as exc:
errors.append(
error(
display_directory,
f"cannot scan skill resources: {exc}",
f"restore readable directory permissions for {display_directory}",
)
)
return files, errors
def _codex_guidance_scan_error(root: Path, exc: OSError) -> str:
path = Path(exc.filename) if exc.filename else root
try:
relative_path = path.relative_to(root).as_posix() or "."
except ValueError:
relative_path = "."
display_path = _one_line_path(relative_path)
if display_path != relative_path:
return _invalid_discovered_path_error(relative_path)
return error(
display_path,
f"cannot scan for Codex guidance: {exc}",
f"restore readable directory permissions for {display_path}",
)
def validate_codex_guidance_layout(
root: Path, _manifest: dict[str, object]
) -> list[str]:
errors: list[str] = []
root = root.resolve()
scan_errors: list[OSError] = []
for directory, directory_names, file_names in os.walk(
root,
topdown=True,
followlinks=False,
onerror=scan_errors.append,
):
directory_names[:] = [name for name in directory_names if name != ".git"]
directory_path = Path(directory)
for filename in file_names:
if filename not in {"AGENTS.md", "AGENTS.override.md"}:
continue
path = directory_path / filename
relative_path = path.relative_to(root).as_posix()
display_path = _one_line_path(relative_path)
if display_path != relative_path:
errors.append(_invalid_discovered_path_error(relative_path))
continue
if filename == "AGENTS.override.md":
errors.append(
error(
display_path,
"AGENTS.override.md is unsupported by the root-only "
"public policy layout",
f"remove {display_path} and move scoped guidance to a "
"manifest-declared .github/instructions/**/*.instructions.md "
"surface",
)
)
elif relative_path != "AGENTS.md":
errors.append(
error(
display_path,
"nested AGENTS.md is unsupported by the root-only "
"public policy layout",
f"remove {display_path} and move scoped guidance to a "
"manifest-declared .github/instructions/**/*.instructions.md "
"surface",
)
)
errors.extend(_codex_guidance_scan_error(root, exc) for exc in scan_errors)
return errors
def _files_for_public_scan(
root: Path, manifest: dict[str, object]
) -> tuple[dict[str, Path], list[str]]:
files: dict[str, Path] = {}
errors: list[str] = []
root = root.resolve()
for surface in _declared_surfaces(manifest):
relative_path = _surface_path(surface)
if relative_path is None:
continue
inventory_path = _inventory_path(relative_path)
expected_kind = _expected_inventory_kind(relative_path)
if expected_kind == "decision":
continue
if inventory_path in PUBLIC_SCAN_EXEMPTIONS:
continue
resolved = _resolve_declared_path(root, relative_path)
if resolved is None:
continue
try:
if not resolved.is_file():
continue
except OSError:
continue
files[relative_path] = resolved
if expected_kind != "skill":
continue
skill_files, skill_errors = _walk_skill_resources(root, resolved.parent)
files.update(skill_files)
errors.extend(skill_errors)
return files, errors
def _manifest_string_values(manifest: object) -> list[str]:
strings: list[str] = []
pending = [manifest]
while pending:
value = pending.pop()
if isinstance(value, str):
strings.append(value)
elif isinstance(value, dict):
pending.extend(value.keys())
pending.extend(value.values())
elif isinstance(value, list):
pending.extend(value)
return strings
def _public_reference_errors(relative_path: str, text: str) -> list[str]:
errors: list[str] = []
casefolded_text = text.casefold()
for token in FORBIDDEN_PUBLIC_TOKENS:
if token.casefold() in casefolded_text:
errors.append(
error(
relative_path,
f"forbidden public token {token!r}",
f"remove {token!r} from {relative_path}",
)
)
for match in FORBIDDEN_PUBLIC_PATH.finditer(text):
forbidden_path = match.group(0)
errors.append(
error(
relative_path,
f"forbidden workspace path {forbidden_path!r}",
f"replace {forbidden_path!r} with a public repository reference",
)
)
return errors
def validate_public_references(root: Path, manifest: dict[str, object]) -> list[str]:
files, errors = _files_for_public_scan(root, manifest)
for value in _manifest_string_values(manifest):
errors.extend(_public_reference_errors(MANIFEST_PATH, value))
for relative_path, path in files.items():
text, read_errors = _read_utf8(path, relative_path)
errors.extend(read_errors)
if text is None:
continue
errors.extend(_public_reference_errors(relative_path, text))
return errors
def validate_public_policy_size(root: Path, _manifest: dict[str, object]) -> list[str]:
policy_path = _resolve_declared_path(root, "AGENTS.md")
if policy_path is None:
return []
try:
if not policy_path.is_file():
return []
byte_size = policy_path.stat().st_size
except OSError:
return []
if byte_size <= PUBLIC_POLICY_BYTE_LIMIT:
return []
return [
error(
"AGENTS.md",
f"public policy is {byte_size:,} bytes; maximum is "
f"{PUBLIC_POLICY_BYTE_LIMIT:,} bytes",
f"reduce AGENTS.md to at most {PUBLIC_POLICY_BYTE_LIMIT:,} bytes",
)
]