@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.
- package/CHANGELOG.md +213 -0
- package/README.md +13 -2
- package/SECURITY.md +23 -8
- package/bin/fabric-agent-adapter.js +50 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py
CHANGED
|
@@ -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
|
|
32
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|