@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +13 -2
  3. package/SECURITY.md +23 -8
  4. package/bin/fabric-agent-adapter.js +50 -5
  5. package/package.json +1 -1
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
  17. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  18. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
  19. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
  80. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
  81. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  86. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  87. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
  88. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  89. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
  90. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
  91. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  92. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  93. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  94. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  95. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  96. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
  97. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  98. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  99. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  100. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  101. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  102. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  103. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  104. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  105. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  106. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  107. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
  108. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
  109. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
  110. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
  111. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
  112. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
  113. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
  114. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
  115. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
  116. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
  117. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
  118. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
@@ -28,8 +28,14 @@ CREDENTIAL_PAIRS = {
28
28
  'session': {'token', 'id'},
29
29
  'code': {'login', 'auth', 'access', 'session'}}
30
30
  JWT_SHAPE = re.compile(r'eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*')
31
- SECRET_VALUE = re.compile(r'(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}'
32
- r'|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})', re.I | re.S)
31
+ # The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key with its
32
+ # prefix in its canonical case after a boundary (the start, a character that is no ASCII letter or digit, or an escape);
33
+ # an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more.
34
+ SECRET_VALUE = re.compile(r'(^[Bb][Ee][Aa][Rr][Ee][Rr]\s.+|(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt"\']|\\u[0-9A-Fa-f]{4}'
35
+ r'|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m'
36
+ r'|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)'
37
+ r'(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})'
38
+ r'|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}))', re.S)
33
39
  LONG_RUN = re.compile(r'[A-Za-z0-9+/=_-]{40,}')
34
40
 
35
41
 
@@ -115,6 +121,11 @@ def check(data):
115
121
  require(all(isinstance(v, str) and 0 < len(v) <= 200 for v in (target, viewer)), 'missing_device')
116
122
  fallback = data.get('fallback', 'if_absent')
117
123
  require(fallback in ('if_absent', 'never'), 'invalid_fallback')
124
+ # DEC-0154 PL-3 and HA-2 (FAA-251): who listens on http_url's port, as the host read it from the socket table
125
+ # (fabric_peer_owner's kinds, or check_service.py's foreign-user). A browser opens a raw loopback address only
126
+ # after that listener is this OS account's; absent, it is unknown.
127
+ owner = data.get('listener_owner', 'unknown')
128
+ require(owner in ('this-account', 'another-account', 'system', 'unknown', 'none', 'foreign-user'), 'invalid_listener_owner')
118
129
  kind = action.get('kind')
119
130
  if kind == 'remote_open':
120
131
  require(set(action) <= {'kind', 'device_id', 'service', 'path'}, 'unexpected_action_field')
@@ -128,17 +139,26 @@ def check(data):
128
139
  require(set(action) == {'kind', 'url'} and action.get('url') == http, 'not_resolved_http_url')
129
140
  require(viewer == target, 'wrong_viewer_device')
130
141
  require(state == 'not_installed' and fallback == 'if_absent', 'browser_fallback_forbidden')
142
+ require(owner == 'this-account', 'listener_not_this_account')
131
143
  else:
132
144
  raise ValueError('unsupported_dashboard_action')
133
145
  return {'ok': True, 'kind': kind}
134
146
 
135
147
 
148
+ def read_input(limit=65536):
149
+ # The handoff's bytes, decoded as UTF-8 with a byte order mark dropped: Windows before Python 3.15 decodes a text
150
+ # stdin with its code page, which misreads a Cyrillic handoff (FAA-210). A stream without bytes is read as text.
151
+ stream = getattr(sys.stdin, 'buffer', None)
152
+ raw = stream.read(limit + 1) if stream is not None else sys.stdin.read(limit + 1)
153
+ require(len(raw) <= limit, 'input_too_large')
154
+ return raw.decode('utf-8-sig') if isinstance(raw, bytes) else raw.lstrip('\ufeff')
155
+
156
+
136
157
  def main():
137
158
  try:
138
- raw = sys.stdin.read(65537)
139
- require(len(raw) <= 65536, 'input_too_large')
140
- result = check(json.loads(raw))
141
- except (ValueError, TypeError, KeyError, AttributeError):
159
+ result = check(json.loads(read_input()))
160
+ # RecursionError: Python 3.9 to 3.13 raise it on a deeply nested document under the cap
161
+ except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
142
162
  # Fixed reasons only: never echo a submitted URL, token or JSON blob.
143
163
  result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
144
164
  print(json.dumps(result))
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env python3
2
+ """Check an agent template folder against the template schema (fabric-workspace S2, decision W0-Q8; FAA-265).
3
+
4
+ python3 template_check.py [--legacy] [--json] <template-folder> [...]
5
+
6
+ Reads only; writes nothing. Exit 0 when no folder has an error, 1 when one has, 2 on a usage error.
7
+
8
+ The rules come from the JSON Schema files in ../schemas/template/, read by a small interpreter of the subset they use,
9
+ so the schema and this check cannot drift apart. On top of the schemas it checks what one file cannot: the header's id
10
+ is the folder's name, manifest.json names the same template and version, every variant a file names is declared in the
11
+ header, TEMPLATE.md inherits the operating rules A1 to A12 by reference, and the folder holds its other parts.
12
+
13
+ --legacy reads the older spellings the first templates used (`class` for `source_class`, `withApproval`, a level
14
+ called `architect`, `spend` for `budget`, ...), names each one as a warning with its canonical form, and checks the
15
+ rest as if it were written canonically. Without it an older spelling is an error.
16
+ """
17
+ import json
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ SCHEMAS = Path(__file__).resolve().parent.parent / "schemas" / "template"
23
+ JSON_FILES = {"manifest.json": "manifest", "connections.json": "connections", "access.json": "access",
24
+ "data-rooms.json": "data-rooms"}
25
+ PROSE_FILES = ("TEMPLATE.md", "assembly.md", "CHANGELOG.md")
26
+ LEVELS = ("owner", "administrator", "project_architect", "member")
27
+ PER_VARIANT = ("placement", "doors", "models", "dashboard", "budget")
28
+
29
+
30
+ def load_schema(name):
31
+ return json.loads((SCHEMAS / ("%s.schema.json" % name)).read_text(encoding="utf-8"))
32
+
33
+
34
+ # A JSON Schema interpreter for the subset the template schemas use. Each problem is (path, message).
35
+
36
+ TYPES = {"object": dict, "array": list, "string": str, "boolean": bool, "null": type(None)}
37
+
38
+
39
+ def _is_type(value, name):
40
+ if name == "integer":
41
+ return isinstance(value, int) and not isinstance(value, bool)
42
+ if name == "number":
43
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
44
+ return isinstance(value, TYPES[name])
45
+
46
+
47
+ def _resolve(ref, root):
48
+ if not ref.startswith("#/"):
49
+ raise ValueError("only local references are supported: %s" % ref)
50
+ node = root
51
+ for part in ref[2:].split("/"):
52
+ node = node[part]
53
+ return node
54
+
55
+
56
+ def check(value, schema, root, path="$"):
57
+ if "$ref" in schema:
58
+ return check(value, _resolve(schema["$ref"], root), root, path)
59
+ problems = []
60
+ if "oneOf" in schema:
61
+ passing = [s for s in schema["oneOf"] if not check(value, s, root, path)]
62
+ if len(passing) != 1:
63
+ best = min((check(value, s, root, path) for s in schema["oneOf"]), key=len)
64
+ return best or [(path, "matches more than one allowed form")]
65
+ kinds = schema.get("type")
66
+ if kinds is not None:
67
+ kinds = kinds if isinstance(kinds, list) else [kinds]
68
+ if not any(_is_type(value, k) for k in kinds):
69
+ return [(path, "must be %s" % " or ".join(kinds))]
70
+ if "enum" in schema and value not in schema["enum"]:
71
+ return [(path, "must be one of %s" % ", ".join(json.dumps(v) for v in schema["enum"]))]
72
+ if isinstance(value, str):
73
+ if len(value) < schema.get("minLength", 0):
74
+ problems.append((path, "must not be empty"))
75
+ if "maxLength" in schema and len(value) > schema["maxLength"]:
76
+ problems.append((path, "is longer than %d characters" % schema["maxLength"]))
77
+ if "pattern" in schema and not re.search(schema["pattern"], value):
78
+ problems.append((path, "does not match %s" % schema["pattern"]))
79
+ if _is_type(value, "number") and "minimum" in schema and value < schema["minimum"]:
80
+ problems.append((path, "must be at least %s" % schema["minimum"]))
81
+ if isinstance(value, list):
82
+ if len(value) < schema.get("minItems", 0):
83
+ problems.append((path, "needs at least %d item(s)" % schema["minItems"]))
84
+ if "maxItems" in schema and len(value) > schema["maxItems"]:
85
+ problems.append((path, "must hold at most %d item(s)" % schema["maxItems"]))
86
+ if schema.get("uniqueItems") and len({json.dumps(v, sort_keys=True) for v in value}) != len(value):
87
+ problems.append((path, "repeats an item"))
88
+ if "items" in schema:
89
+ for i, item in enumerate(value):
90
+ problems += check(item, schema["items"], root, "%s[%d]" % (path, i))
91
+ if isinstance(value, dict):
92
+ if len(value) < schema.get("minProperties", 0):
93
+ problems.append((path, "needs at least %d key(s)" % schema["minProperties"]))
94
+ for key in schema.get("required", ()):
95
+ if key not in value:
96
+ problems.append((path, "lacks %s" % key))
97
+ props = schema.get("properties", {})
98
+ patterns = schema.get("patternProperties", {})
99
+ extra = schema.get("additionalProperties", True)
100
+ for key, item in value.items():
101
+ where = "%s.%s" % (path, key)
102
+ if key in props:
103
+ problems += check(item, props[key], root, where)
104
+ continue
105
+ matched = [p for p in patterns if re.search(p, key)]
106
+ for p in matched:
107
+ problems += check(item, patterns[p], root, where)
108
+ if matched:
109
+ continue
110
+ if extra is False:
111
+ problems.append((where, "is not a key of this file's schema"))
112
+ elif isinstance(extra, dict):
113
+ problems += check(item, extra, root, where)
114
+ return problems
115
+
116
+
117
+ # The header of TEMPLATE.md: front matter holding one `template:` mapping of scalars and inline lists.
118
+
119
+ def _scalar(text):
120
+ text = text.strip()
121
+ if len(text) >= 2 and text[0] == text[-1] and text[0] in "'\"":
122
+ return text[1:-1]
123
+ if text.startswith("[") and text.endswith("]"):
124
+ inner = text[1:-1].strip()
125
+ return [_scalar(part) for part in inner.split(",")] if inner else []
126
+ if re.fullmatch(r"-?(0|[1-9][0-9]*)", text):
127
+ return int(text)
128
+ return text
129
+
130
+
131
+ def read_header(text):
132
+ """The `template:` header as a dict, or raise ValueError naming what is wrong."""
133
+ lines = text.splitlines()
134
+ if not lines or lines[0].strip() != "---":
135
+ raise ValueError("does not start with a front matter block (---)")
136
+ try:
137
+ end = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
138
+ except StopIteration:
139
+ raise ValueError("its front matter block is not closed (---)")
140
+ header, current, last = {}, None, None
141
+ for number, line in enumerate(lines[1:end], start=2):
142
+ if not line.strip() or line.lstrip().startswith("#"):
143
+ continue
144
+ stripped = re.sub(r"\s+#.*$", "", line)
145
+ indent = len(stripped) - len(stripped.lstrip())
146
+ match = re.fullmatch(r"([A-Za-z_][\w-]*):\s*(.*)", stripped.strip())
147
+ if indent and match is None and current is not None and last is not None:
148
+ # A continuation line of a long inline list.
149
+ previous = current[last]
150
+ if isinstance(previous, str) and previous.startswith("["):
151
+ current[last] = previous + " " + stripped.strip()
152
+ continue
153
+ if match is None:
154
+ raise ValueError("line %d is not `key: value`" % number)
155
+ key, value = match.groups()
156
+ if indent == 0:
157
+ if value:
158
+ header[key] = _scalar(value)
159
+ current = None
160
+ else:
161
+ current = header[key] = {}
162
+ last = None
163
+ elif current is None:
164
+ raise ValueError("line %d is indented under a key that holds a value" % number)
165
+ else:
166
+ current[key] = value
167
+ last = key
168
+ for block in header.values():
169
+ if isinstance(block, dict):
170
+ for key, value in list(block.items()):
171
+ if isinstance(value, str) and value.startswith("[") and not value.endswith("]"):
172
+ raise ValueError("%s's list is not closed" % key)
173
+ block[key] = _scalar(value) if isinstance(value, str) else value
174
+ return header
175
+
176
+
177
+ # Older spellings (--legacy): each rewrite returns the canonical value and appends (path, message) warnings.
178
+
179
+ ARCHITECT = {"project architect", "project-architect", "architect", "projectArchitect"}
180
+ CAMEL = {"withApproval": "with_approval", "withoutAsking": "without_asking"}
181
+ CALLS = ("call", "ask", "respond", "proxy", "transport", "inference", "local")
182
+
183
+
184
+ def _direction(text):
185
+ words = text.lower()
186
+ reads = "read" in words
187
+ writes = any(w in words for w in ("write", "spend", "sign", "send", "draw", "release", "upload", "order", "reply"))
188
+ if reads and writes:
189
+ return "read-write"
190
+ if reads:
191
+ return "read"
192
+ if writes:
193
+ return "write"
194
+ if any(w in words for w in CALLS):
195
+ return "call"
196
+ return None
197
+
198
+
199
+ def _prose_to_enum(item, key, path, warnings, target):
200
+ value = item.pop(key)
201
+ if not isinstance(value, str):
202
+ item[target] = value
203
+ return
204
+ enum = value if value in ("read", "write", "read-write", "call") else _direction(value)
205
+ if enum is None:
206
+ item[target] = value
207
+ return
208
+ item[target] = enum
209
+ if enum != value or key != target:
210
+ warnings.append(("%s.%s" % (path, key), "write %s: %s%s" % (
211
+ target, json.dumps(enum), (", scope: %s" % json.dumps(value)) if enum != value else "")))
212
+ if enum != value and "scope" not in item:
213
+ item["scope"] = value
214
+
215
+
216
+ def _legacy_variants(item, path, warnings):
217
+ key = "variants" if "variants" in item else "variant" if "variant" in item else None
218
+ if key is None:
219
+ return
220
+ value = item.pop(key)
221
+ values = value if isinstance(value, list) else [value]
222
+ split = [v.strip() for part in values if isinstance(part, str) for v in part.split(",") if v.strip()]
223
+ split += [v for v in values if not isinstance(v, str)]
224
+ if any(isinstance(v, str) and v.lower() in ("all", "both", "every", "any") for v in split):
225
+ warnings.append(("%s.%s" % (path, key), "every variant: leave variants out"))
226
+ return
227
+ item["variants"] = split
228
+ if key != "variants" or split != value:
229
+ warnings.append(("%s.%s" % (path, key), "write variants as a list of variant ids"))
230
+
231
+
232
+ def legacy_connections(items, warnings):
233
+ if not isinstance(items, list):
234
+ return items
235
+ for i, item in enumerate(items):
236
+ if not isinstance(item, dict):
237
+ continue
238
+ path = "$[%d]" % i
239
+ if "class" in item and "source_class" not in item:
240
+ item["source_class"] = item.pop("class")
241
+ warnings.append((path + ".class", "write source_class"))
242
+ _legacy_variants(item, path, warnings)
243
+ for key in ("direction", "mode", "access"):
244
+ if key in item and (key != "direction" or item[key] not in ("read", "write", "read-write", "call")):
245
+ if key != "direction" and "direction" in item:
246
+ continue
247
+ _prose_to_enum(item, key, path, warnings, "direction")
248
+ break
249
+ required = item.get("required")
250
+ if isinstance(required, str):
251
+ lowered = required.lower()
252
+ if lowered.startswith(("optional", "required")):
253
+ item["required"] = lowered.startswith("required")
254
+ rest = re.sub(r"^(optional|required)[\s,:;(]*", "", required, flags=re.I).rstrip(")").strip()
255
+ if rest and "when" not in item:
256
+ item["when"] = rest
257
+ warnings.append((path + ".required", "write required as true or false%s" % (
258
+ ", the condition in when" if rest else "")))
259
+ return items
260
+
261
+
262
+ def legacy_access(doc, warnings):
263
+ if not isinstance(doc, dict):
264
+ return doc
265
+ if "levels" not in doc:
266
+ found = [k for k in list(doc) if k in LEVELS or k in ARCHITECT]
267
+ if found:
268
+ doc["levels"] = {k: doc.pop(k) for k in found}
269
+ warnings.append(("$", "put the levels under levels"))
270
+ levels = doc.get("levels")
271
+ if isinstance(levels, dict):
272
+ for key in list(levels):
273
+ if key in ARCHITECT:
274
+ levels["project_architect"] = levels.pop(key)
275
+ warnings.append(("$.levels.%s" % key, "write project_architect"))
276
+ for name, level in levels.items():
277
+ if isinstance(level, dict):
278
+ for key in list(level):
279
+ if key in CAMEL:
280
+ level[CAMEL[key]] = level.pop(key)
281
+ warnings.append(("$.levels.%s.%s" % (name, key), "write %s" % CAMEL[key]))
282
+ for key in list(doc):
283
+ if key != "never" and key.startswith("never") and isinstance(doc[key], list):
284
+ doc.setdefault("never", [])
285
+ doc["never"] = doc["never"] + doc.pop(key)
286
+ warnings.append(("$.%s" % key, "write never"))
287
+ if "never" not in doc:
288
+ doc["never"] = []
289
+ warnings.append(("$", "add never: what the agent never does at any level ([] when nothing)"))
290
+ allowed = set(load_schema("access")["properties"])
291
+ for key in list(doc):
292
+ if key not in allowed and not key.startswith("x-"):
293
+ slug = "x-" + re.sub(r"[^a-z0-9]+", "-", key.lower()).strip("-")
294
+ doc[slug] = doc.pop(key)
295
+ warnings.append(("$.%s" % key, "a key of this template's own: write %s" % slug))
296
+ return doc
297
+
298
+
299
+ def legacy_rooms(items, warnings):
300
+ if not isinstance(items, list):
301
+ return items
302
+ for i, item in enumerate(items):
303
+ if not isinstance(item, dict):
304
+ continue
305
+ path = "$[%d]" % i
306
+ if "block" in item and "blocks" not in item:
307
+ value = item.pop("block")
308
+ item["blocks"] = value if isinstance(value, list) else [value]
309
+ warnings.append((path + ".block", "write blocks as a list"))
310
+ _legacy_variants(item, path, warnings)
311
+ if "writers" in item and "notes" not in item:
312
+ item["notes"] = "writers: " + ", ".join(map(str, item.pop("writers")))
313
+ warnings.append((path + ".writers", "a room's writers belong in the room's own template; noted"))
314
+ if "why" in item and "use" not in item:
315
+ item["use"] = item.pop("why")
316
+ warnings.append((path + ".why", "write use"))
317
+ if "access" in item and item["access"] not in ("read", "write", "read-write"):
318
+ _prose_to_enum(item, "access", path, warnings, "access")
319
+ return items
320
+
321
+
322
+ def _legacy_protocol(entry, path, warnings):
323
+ if isinstance(entry, dict):
324
+ for key in ("checkedAgainst", "checked"):
325
+ if key in entry and "checked_against" not in entry:
326
+ entry["checked_against"] = entry.pop(key)
327
+ warnings.append(("%s.%s" % (path, key), "write checked_against"))
328
+
329
+
330
+ def legacy_manifest(doc, warnings):
331
+ if not isinstance(doc, dict):
332
+ return doc
333
+ if "spend" in doc and "budget" not in doc:
334
+ doc["budget"] = doc.pop("spend")
335
+ warnings.append(("$.spend", "write budget"))
336
+ contract = doc.get("contract")
337
+ if isinstance(contract, dict):
338
+ if "name" in contract or "checkedAgainst" in contract or "checked" in contract:
339
+ _legacy_protocol(contract, "$.contract", warnings)
340
+ else:
341
+ for key, entry in contract.items():
342
+ _legacy_protocol(entry, "$.contract.%s" % key, warnings)
343
+ for key in ("interop",):
344
+ if key in doc and isinstance(contract, dict) and "name" not in contract:
345
+ contract[key] = doc.pop(key)
346
+ warnings.append(("$.%s" % key, "put it in contract.%s" % key))
347
+ _legacy_protocol(contract[key], "$.contract.%s" % key, warnings)
348
+ allowed = set(load_schema("manifest")["properties"])
349
+ for key in list(doc):
350
+ if key not in allowed and not key.startswith("x-"):
351
+ slug = "x-" + re.sub(r"[^a-z0-9]+", "-", key.lower()).strip("-")
352
+ doc[slug] = doc.pop(key)
353
+ warnings.append(("$.%s" % key, "a key of this template's own: write %s" % slug))
354
+ return doc
355
+
356
+
357
+ # Gaps no rewrite can fill: a warning under --legacy, an error otherwise.
358
+ LEGACY_GAPS = {("connections", "$[]", "lacks direction"), ("manifest", "$", "lacks version")}
359
+
360
+ LEGACY = {"connections": legacy_connections, "access": legacy_access, "data-rooms": legacy_rooms,
361
+ "manifest": legacy_manifest}
362
+
363
+
364
+ def _variants_named(doc):
365
+ named = set()
366
+ if isinstance(doc, list):
367
+ for item in doc:
368
+ if isinstance(item, dict) and isinstance(item.get("variants"), list):
369
+ named.update(v for v in item["variants"] if isinstance(v, str))
370
+ return named
371
+
372
+
373
+ def check_folder(folder, legacy=False):
374
+ """Every finding for one template folder: dicts {level, file, path, message}."""
375
+ folder = Path(folder)
376
+ findings = []
377
+
378
+ def add(level, file, path, message):
379
+ findings.append({"level": level, "file": file, "path": path, "message": message})
380
+
381
+ if not folder.is_dir():
382
+ add("error", ".", "$", "is not a folder")
383
+ return findings
384
+ header = None
385
+ template_md = folder / "TEMPLATE.md"
386
+ for name in PROSE_FILES:
387
+ path = folder / name
388
+ if not path.is_file():
389
+ add("error", name, "$", "is missing")
390
+ elif not path.read_text(encoding="utf-8", errors="replace").strip():
391
+ add("error", name, "$", "is empty")
392
+ if template_md.is_file():
393
+ text = template_md.read_text(encoding="utf-8", errors="replace")
394
+ try:
395
+ header = read_header(text)
396
+ except ValueError as error:
397
+ add("error", "TEMPLATE.md", "$", str(error))
398
+ if header is not None:
399
+ schema = load_schema("template-header")
400
+ for where, message in check(header, schema, schema):
401
+ add("error", "TEMPLATE.md", where, message)
402
+ if not (re.search(r"\bA1\b", text) and re.search(r"\bA12\b", text)):
403
+ add("error", "TEMPLATE.md", "$", "does not inherit the operating rules A1 to A12 by reference")
404
+ meta = header.get("template") if isinstance(header, dict) else None
405
+ meta = meta if isinstance(meta, dict) else {}
406
+ template_id, version = meta.get("id"), meta.get("version")
407
+ declared = set(meta.get("variants") or []) if isinstance(meta.get("variants"), list) else set()
408
+ if isinstance(template_id, str) and template_id != folder.resolve().name:
409
+ add("error", "TEMPLATE.md", "$.template.id", "is %s, the folder is %s" % (
410
+ json.dumps(template_id), json.dumps(folder.resolve().name)))
411
+ checks = folder / "checks"
412
+ if not checks.is_dir() or not any(p.is_file() for p in checks.rglob("*")):
413
+ add("error", "checks/", "$", "holds no acceptance check")
414
+ changelog = folder / "CHANGELOG.md"
415
+ if isinstance(version, str) and changelog.is_file() and version not in changelog.read_text(
416
+ encoding="utf-8", errors="replace"):
417
+ add("error", "CHANGELOG.md", "$", "has no entry for version %s" % version)
418
+
419
+ docs = {}
420
+ for file, schema_name in JSON_FILES.items():
421
+ path = folder / file
422
+ if not path.is_file():
423
+ add("error", file, "$", "is missing")
424
+ continue
425
+ try:
426
+ doc = json.loads(path.read_text(encoding="utf-8"))
427
+ except (ValueError, UnicodeDecodeError) as error:
428
+ add("error", file, "$", "is not JSON: %s" % error)
429
+ continue
430
+ if legacy:
431
+ warnings = []
432
+ doc = LEGACY[schema_name](doc, warnings)
433
+ for where, message in warnings:
434
+ add("warning", file, where, message)
435
+ schema = load_schema(schema_name)
436
+ for where, message in check(doc, schema, schema):
437
+ gap = legacy and (schema_name, re.sub(r"\[\d+\]", "[]", where), message) in LEGACY_GAPS
438
+ add("warning" if gap else "error", file, where, message + ("; required from schema 1 on" if gap else ""))
439
+ docs[schema_name] = doc
440
+
441
+ manifest = docs.get("manifest")
442
+ if isinstance(manifest, dict):
443
+ if template_id is not None and manifest.get("template") not in (None, template_id):
444
+ add("error", "manifest.json", "$.template", "is %s, TEMPLATE.md names %s" % (
445
+ json.dumps(manifest.get("template")), json.dumps(template_id)))
446
+ if version is not None and manifest.get("version") not in (None, version):
447
+ add("error", "manifest.json", "$.version", "is %s, TEMPLATE.md names %s" % (
448
+ json.dumps(manifest.get("version")), json.dumps(version)))
449
+ variants = manifest.get("variants") if isinstance(manifest.get("variants"), dict) else {}
450
+ for name in sorted(set(variants) - declared):
451
+ add("error", "manifest.json", "$.variants.%s" % name, "is not a variant TEMPLATE.md declares")
452
+ for key in PER_VARIANT:
453
+ if key in manifest:
454
+ continue
455
+ missing = [v for v in sorted(variants) if not isinstance(variants[v], dict) or key not in variants[v]]
456
+ if not variants:
457
+ add("error", "manifest.json", "$", "lacks %s" % key)
458
+ elif missing:
459
+ add("error", "manifest.json", "$", "lacks %s, at the top or in variants %s" % (key, ", ".join(missing)))
460
+ for file, schema_name in (("connections.json", "connections"), ("data-rooms.json", "data-rooms")):
461
+ for name in sorted(_variants_named(docs.get(schema_name)) - declared):
462
+ add("error", file, "$", "names variant %s that TEMPLATE.md does not declare" % json.dumps(name))
463
+ return findings
464
+
465
+
466
+ def main(argv):
467
+ args = [a for a in argv if not a.startswith("--")]
468
+ flags = {a for a in argv if a.startswith("--")}
469
+ unknown = flags - {"--legacy", "--json"}
470
+ if unknown or not args:
471
+ sys.stderr.write("usage: template_check.py [--legacy] [--json] <template-folder> [...]\n")
472
+ return 2
473
+ report, failed = {}, False
474
+ for folder in args:
475
+ findings = check_folder(folder, legacy="--legacy" in flags)
476
+ report[folder] = findings
477
+ failed = failed or any(f["level"] == "error" for f in findings)
478
+ if "--json" in flags:
479
+ print(json.dumps({"ok": not failed, "folders": report}, indent=2, ensure_ascii=False))
480
+ else:
481
+ for folder, findings in report.items():
482
+ errors = sum(f["level"] == "error" for f in findings)
483
+ warnings = len(findings) - errors
484
+ print("%s: %s" % (folder, "ok" if not findings else "%d error(s), %d warning(s)" % (errors, warnings)))
485
+ for f in findings:
486
+ print(" %s %s %s: %s" % (f["level"], f["file"], f["path"], f["message"]))
487
+ return 1 if failed else 0
488
+
489
+
490
+ if __name__ == "__main__":
491
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ The first version as a template folder (FAA-265): the prompt released in the adapter's 0.75.0 as
6
+ `references/communication-agent-template.md`, moved unchanged into `TEMPLATE.md`, with its manifest, connections, access
7
+ levels, data rooms, acceptance check and assembly.