@passioncode-ai/fabric-agent-adapter 0.80.2 → 0.82.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 (92) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +18 -5
  3. package/SECURITY.md +25 -8
  4. package/bin/fabric-agent-adapter.js +24 -1
  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/dashboard-links.md +16 -3
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +116 -4
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +33 -3
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +257 -10
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -77
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +899 -0
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -4
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +23 -5
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +16 -3
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +5 -2
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +28 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -3
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +47 -10
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +31 -8
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +33 -3
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +19 -28
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +9 -6
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +146 -3
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +3 -2
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +79 -2
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +269 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +64 -5
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -7
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +73 -3
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +89 -22
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +39 -6
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -0
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +76 -11
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +8 -153
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +86 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +324 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +34 -3
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +35 -17
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +77 -4
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +156 -133
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +257 -10
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +64 -3
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +246 -24
  64. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +16 -6
  65. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  66. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  67. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +16 -3
  68. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  69. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  70. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  71. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  72. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  73. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  74. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  75. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  76. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +33 -3
  77. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  78. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  79. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  80. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  81. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  86. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  87. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
  88. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +16 -8
  89. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +46 -31
  90. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
  91. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +18 -16
  92. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -13
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ """Create a Fabric agent project with the contract built in, in one command (FAA-270, North Star G21).
3
+
4
+ python3 new_agent.py <folder> --id <agent-id> [--profile mcp|a2a|local-runner] [--service]
5
+ [--capability NAME] [--effect CLASS] [--force] [--json]
6
+
7
+ It writes, into an empty (or new) folder:
8
+
9
+ - the provider bundle `adapt_project.py scaffold` writes, pinned to the contract (`fabric-agent.json`,
10
+ `fabric-contract.lock.json`, `fabric/`), every value it invents a sentinel `adapt_project.py check` names;
11
+ - `fabric.source.json`, the agent's source manifest (DEC-0149, `fabric-source/1`): what is source, default deny, and the
12
+ slots an installation fills (the publisher's URI behind `provider.createdBy`, the placement record and, for a
13
+ service, its supervisor label and port), never a value;
14
+ - the placement guard (`src/placement.py`, with its tests in `tests/`): at start the agent reads Project Observatory's
15
+ placement record (`observatory-placement/1`) and refuses to start where the record places it on another machine,
16
+ starts degraded when it cannot read it, and starts when there is none; `config/placement.example.json` shows the
17
+ record's shape on neutral values;
18
+ - `README.md`, `LICENSE` (all rights reserved until the owner names the terms), `.gitignore`;
19
+ - with `--service`: `service/`, the service kit's sample (`service.py`, a fabric-service/0.1 service with one capability
20
+ and a job) and the kit modules it imports, copied by `fabric_kit.py install`, whose digests `fabric_kit.py status`
21
+ checks later.
22
+
23
+ A repeat with the same arguments in a folder this command made changes nothing and answers the same line. A folder that
24
+ holds anything else is refused unless `--force`, which writes the planned files over it and leaves every other file as
25
+ it is. The last stdout line is `{"folder": "<path>", "id": "<id>", "profile": "<profile>", "service": <bool>,
26
+ "created": [<paths>], "repeated": <bool>}` (`--json` prints only it). Exit 0 made or already made, 1 refused (the
27
+ reason on stderr), 2 usage. Writes nothing outside the folder; reads no key and no instance data. Standard library only.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ from pathlib import Path
35
+ import re
36
+ import shutil
37
+ import sys
38
+ from typing import Any, Dict, List, Optional, Sequence
39
+
40
+ HERE = Path(__file__).resolve().parent
41
+ sys.path.insert(0, str(HERE))
42
+ import adapt_project as ap # noqa: E402 - the scaffolder and the source manifest, beside this file
43
+
44
+ VERSION = "0.1.0"
45
+ RECORD = "fabric/new-agent.json"
46
+ #: The agent's id: the package's `name` grammar (`fabric-package.schema.json#/$defs/name`), kebab case.
47
+ AGENT_ID = re.compile(r"[a-z][a-z0-9]*(?:-[a-z0-9]+)*\Z")
48
+ CAPABILITY = re.compile(r"[a-z][a-z0-9._-]{1,127}\Z")
49
+ KIT = HERE.parent.parent / "building-fabric-services" / "scripts"
50
+ #: What the sample service imports; `fabric_kit.py install` copies each one's closure.
51
+ SERVICE_MODULES = ("fabric_service", "fabric_interop", "fabric_board", "fabric_hub")
52
+
53
+
54
+ class Refused(Exception):
55
+ """The folder cannot be made as asked; the reason is printed, never a value."""
56
+
57
+
58
+ PLACEMENT_PY = '''"""The placement guard: at start, before the agent polls, schedules or listens, it reads Project Observatory's
59
+ placement record and runs only on the machine that record names (creating-fabric-agents, where-agents-run.md).
60
+
61
+ verdict(service_id, label) -> {"state": "ok" | "placed-elsewhere" | "placement-unreadable", "machine"?}
62
+
63
+ `placed-elsewhere`: the record's `elsewhere` list names this agent (its `serviceId` or its supervisor `label`) on a
64
+ machine that is not this one: refuse to start, log one line, report it as degraded. `placement-unreadable`: the file is
65
+ there and cannot be read, or is of another schema: start, and report it as degraded. No file: start.
66
+ """
67
+ import json
68
+ import os
69
+ from pathlib import Path
70
+ from typing import Any, Dict, Optional
71
+
72
+ SCHEMA = "observatory-placement/1"
73
+
74
+
75
+ def placement_path(env: Optional[Dict[str, str]] = None) -> Path:
76
+ env = os.environ if env is None else env
77
+ base = env.get("XDG_DATA_HOME") or ""
78
+ root = Path(base) if os.path.isabs(base) else Path.home() / ".local" / "share"
79
+ return root / "project-observatory" / "placement.json"
80
+
81
+
82
+ def verdict(service_id: str, label: str, path: Optional[Path] = None) -> Dict[str, Any]:
83
+ path = placement_path() if path is None else path
84
+ try:
85
+ data = path.read_bytes()
86
+ except FileNotFoundError:
87
+ return {"state": "ok"}
88
+ except OSError:
89
+ return {"state": "placement-unreadable"}
90
+ try:
91
+ doc = json.loads(data.decode("utf-8"))
92
+ except ValueError:
93
+ return {"state": "placement-unreadable"}
94
+ if not isinstance(doc, dict) or doc.get("schema") != SCHEMA or not isinstance(doc.get("elsewhere"), list):
95
+ return {"state": "placement-unreadable"}
96
+ for entry in doc["elsewhere"]:
97
+ if not isinstance(entry, dict):
98
+ continue
99
+ ours = (service_id and entry.get("serviceId") == service_id) or (label and entry.get("label") == label)
100
+ if ours and entry.get("placedOn") and entry.get("placedOn") != doc.get("machine"):
101
+ return {"state": "placed-elsewhere", "machine": entry.get("placedOn")}
102
+ return {"state": "ok"}
103
+ '''
104
+
105
+ PLACEMENT_TEST = '''"""The placement guard: one copy placed elsewhere refuses, one with no file and one with a malformed file start."""
106
+ import json
107
+ from pathlib import Path
108
+ import sys
109
+ import tempfile
110
+ import unittest
111
+
112
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
113
+ import placement # noqa: E402
114
+
115
+ EXAMPLE = Path(__file__).resolve().parents[1] / "config" / "placement.example.json"
116
+
117
+
118
+ class PlacementTests(unittest.TestCase):
119
+ def setUp(self):
120
+ self.folder = tempfile.TemporaryDirectory()
121
+ self.addCleanup(self.folder.cleanup)
122
+ self.path = Path(self.folder.name) / "placement.json"
123
+
124
+ def test_placed_elsewhere_refuses(self):
125
+ self.path.write_text(EXAMPLE.read_text(encoding="utf-8"), encoding="utf-8")
126
+ found = placement.verdict("{service_id}", "{label}", self.path)
127
+ self.assertEqual(found["state"], "placed-elsewhere")
128
+
129
+ def test_no_file_starts(self):
130
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path), {{"state": "ok"}})
131
+
132
+ def test_a_malformed_file_starts_degraded(self):
133
+ self.path.write_text("{{not json", encoding="utf-8")
134
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path), {{"state": "placement-unreadable"}})
135
+ self.path.write_text(json.dumps({{"schema": "another/1"}}), encoding="utf-8")
136
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path)["state"], "placement-unreadable")
137
+
138
+ def test_placed_on_this_machine_starts(self):
139
+ doc = json.loads(EXAMPLE.read_text(encoding="utf-8"))
140
+ doc["machine"] = doc["elsewhere"][0]["placedOn"]
141
+ self.path.write_text(json.dumps(doc), encoding="utf-8")
142
+ self.assertEqual(placement.verdict("{service_id}", "{label}", self.path), {{"state": "ok"}})
143
+
144
+
145
+ if __name__ == "__main__":
146
+ unittest.main()
147
+ '''
148
+
149
+ README = '''# {agent_id}
150
+
151
+ A Fabric agent made with the Fabric Agent Adapter's `new` command, contract `{contract}` at `{commit}`.
152
+
153
+ ## Layout
154
+
155
+ - `fabric-agent.json`, `fabric-contract.lock.json`, `fabric/`: the provider bundle, pinned to the contract. Every value
156
+ the scaffold invented is a sentinel: `adapt_project.py check` names each one until it is replaced.
157
+ - `fabric.source.json`: the agent's source manifest (DEC-0149): what is source, and the slots an installation fills,
158
+ never a value. `fabric_source_check.py check .` reads the source under it (exit 0 before every release).
159
+ - `src/placement.py`: the placement guard the agent runs at start, before it polls, schedules or listens.
160
+ {service_line}
161
+ ## Configure
162
+
163
+ Keys are read by name from the environment or the secret store, never from a file of this repository. Slots:
164
+ {slots}
165
+
166
+ ## Data
167
+
168
+ The agent's data lives apart from this source, in its data home (`<agent>-data`, or the service kit's folders), from
169
+ the first commit.
170
+ '''
171
+
172
+ GITIGNORE = """# the instance: real configuration, keys and what running the agent leaves
173
+ config/*.json
174
+ !config/*.example.json
175
+ .env
176
+ .env.*
177
+ !.env.example
178
+ *.token
179
+ *.credential
180
+ __pycache__/
181
+ *.pyc
182
+ """
183
+
184
+
185
+ def _placement_example(service_id: str, label: str) -> str:
186
+ return ap._json_text({"schema": "observatory-placement/1", "machine": "example-machine-a", "updatedAt": "2026-01-01T00:00:00Z",
187
+ "elsewhere": [{"label": label, "serviceId": service_id, "placedOn": "example-machine-b",
188
+ "since": "2026-01-01T00:00:00Z", "by": "full machine-move leave"}]})
189
+
190
+
191
+ def plan(args: argparse.Namespace) -> Dict[str, str]:
192
+ """Every file the command writes, by its path, with its text (the scaffold's own included)."""
193
+ agent_id = args.id
194
+ capability = args.capability or "%s.run" % agent_id.replace("-", "_")
195
+ if not CAPABILITY.match(capability):
196
+ raise Refused("--capability %r is not a capability name (lower case, `domain.action`)" % capability)
197
+ scaffold = argparse.Namespace(
198
+ profile=args.profile, provider_id="urn:fabric:provider:%s" % agent_id, provider_name=agent_id,
199
+ capability_id="urn:fabric:capability:%s" % capability, capability_name=capability,
200
+ schema_base="https://replace.invalid/%s/schemas" % agent_id, mcp_mode="streamable-http",
201
+ mcp_url="https://replace.invalid/mcp", agent_card_url="https://replace.invalid/.well-known/agent-card.json",
202
+ runner_kind="replace-me", executable_ref="urn:executable:replace-me", runtime_arg=[], effect=args.effect,
203
+ idempotency="none", cost_preset=[], default_cost_preset=None, job=False, created_by=None, identity_method=None,
204
+ data_class=[], probe_assertion=[])
205
+ files = dict(ap._generated_files(scaffold))
206
+ service_id = "%s.default" % agent_id
207
+ label = "com.example.%s" % agent_id
208
+ include = ["README.md", "LICENSE", ".gitignore", "src/", "tests/", "config/placement.example.json",
209
+ "fabric-agent.json", "fabric-contract.lock.json", "fabric/"] + (["service/"] if args.service else [])
210
+ manifest = ap.source_manifest(include, "%s, a Fabric agent" % agent_id, service=args.service)
211
+ slots = "\n".join("- `%s` (%s%s): %s" % (s["name"], s["kind"], ", optional" if s.get("optional") else "", s["purpose"])
212
+ for s in manifest["slots"])
213
+ files.update({
214
+ ap.SOURCE_FILE: ap._json_text(manifest),
215
+ "README.md": README.format(agent_id=agent_id, contract=ap.CONTRACT_VERSION, commit=ap.CONTRACT_COMMIT[:12], slots=slots,
216
+ service_line="- `service/`: the service kit's sample, run as `python3 service/service.py "
217
+ "serve --id %s --port <port> --data-dir <data>`, and the kit modules it imports.\n" % agent_id
218
+ if args.service else ""),
219
+ "LICENSE": "Copyright (c) the owner of %s. All rights reserved, until the owner names other terms.\n" % agent_id,
220
+ ".gitignore": GITIGNORE,
221
+ "src/placement.py": PLACEMENT_PY,
222
+ "tests/test_placement.py": PLACEMENT_TEST.format(service_id=service_id, label=label),
223
+ "config/placement.example.json": _placement_example(service_id, label),
224
+ })
225
+ record = {"tool": "fabric-agent-adapter new %s" % VERSION, "id": agent_id, "profile": args.profile, "service": args.service,
226
+ "capability": capability, "effect": args.effect, "contract": ap.CONTRACT_COMMIT}
227
+ files[RECORD] = ap._json_text(record)
228
+ return files
229
+
230
+
231
+ def _service_files(folder: Path) -> List[str]:
232
+ """Copy the kit's sample and the closure of what it imports into `service/` (fabric_kit.py install)."""
233
+ import importlib.util
234
+ found = importlib.util.spec_from_file_location("fabric_kit", KIT / "fabric_kit.py") # the kit's own installer
235
+ fabric_kit = importlib.util.module_from_spec(found)
236
+ found.loader.exec_module(fabric_kit) # type: ignore[union-attr]
237
+ target = folder / "service"
238
+ result = fabric_kit.install(target, "python", list(SERVICE_MODULES))
239
+ if result["refused"]:
240
+ raise Refused("service/ holds kit modules this kit does not recognise: %s" % ", ".join(result["refused"]))
241
+ shutil.copyfile(KIT / "sample_service.py", target / "service.py")
242
+ return ["service/" + name for name in result["copied"] + result["kept"]] + ["service/service.py"]
243
+
244
+
245
+ def make(folder: Path, args: argparse.Namespace) -> Dict[str, Any]:
246
+ if not AGENT_ID.match(args.id) or len(args.id) > 64:
247
+ raise Refused("--id %r is not an agent id (lower case words joined by -, at most 64 characters)" % args.id)
248
+ files = plan(args)
249
+ answer = {"folder": str(folder), "id": args.id, "profile": args.profile, "service": args.service}
250
+ if folder.exists() and not folder.is_dir():
251
+ raise Refused("%s is not a folder" % folder.name)
252
+ present = sorted(p.relative_to(folder).as_posix() for p in folder.rglob("*")) if folder.is_dir() else []
253
+ if present:
254
+ record = folder / RECORD
255
+ try:
256
+ same = record.is_file() and json.loads(record.read_text(encoding="utf-8")) == json.loads(files[RECORD])
257
+ except ValueError:
258
+ same = False
259
+ if same and all((folder / path).is_file() for path in files) and not args.force:
260
+ return dict(answer, created=[], repeated=True)
261
+ if not args.force:
262
+ raise Refused("the folder is not empty and was not made by this command with these arguments: "
263
+ "name another folder, or pass --force to write the planned files over it")
264
+ for path, text in files.items():
265
+ target = folder / path
266
+ target.parent.mkdir(parents=True, exist_ok=True)
267
+ target.write_text(text, encoding="utf-8")
268
+ created = sorted(files)
269
+ if args.service:
270
+ created = sorted(created + _service_files(folder))
271
+ return dict(answer, created=created, repeated=False)
272
+
273
+
274
+ def build_parser() -> argparse.ArgumentParser:
275
+ parser = argparse.ArgumentParser(prog="new_agent.py", description="Create a Fabric agent project with the contract built in.")
276
+ parser.add_argument("folder", type=Path)
277
+ parser.add_argument("--id", required=True, help="the agent's id, kebab case (example-agent)")
278
+ parser.add_argument("--profile", choices=("mcp", "a2a", "local-runner"), default="mcp")
279
+ parser.add_argument("--service", action="store_true", help="also wire the service kit's sample (fabric-service/0.1)")
280
+ parser.add_argument("--capability", help="its capability, `domain.action` (default <id>.run)")
281
+ parser.add_argument("--effect", choices=ap.EFFECT_CLASSES, default="none",
282
+ help="the capability's effect class (default none: declare money, deletion or publication now)")
283
+ parser.add_argument("--force", action="store_true", help="write the planned files into a folder that holds others")
284
+ parser.add_argument("--json", action="store_true", help="print only the last line")
285
+ return parser
286
+
287
+
288
+ def main(argv: Optional[Sequence[str]] = None) -> int:
289
+ try:
290
+ args = build_parser().parse_args(argv)
291
+ except SystemExit as exc:
292
+ return exc.code if isinstance(exc.code, int) else 2
293
+ try:
294
+ answer = make(args.folder.expanduser().resolve(), args)
295
+ except (Refused, ap.AdaptationError, OSError) as exc:
296
+ print("new_agent.py: %s" % (exc if not isinstance(exc, OSError) else "the folder cannot be written (%s)"
297
+ % type(exc).__name__), file=sys.stderr)
298
+ return 1
299
+ if not args.json:
300
+ print("%s %s: %d file(s)%s" % ("already made" if answer["repeated"] else "made", args.id, len(answer["created"]),
301
+ "" if answer["repeated"] else "; next: replace the sentinels `adapt_project.py check` "
302
+ "names, then run fabric_source_check.py check on the folder"))
303
+ print(json.dumps(answer, ensure_ascii=False))
304
+ return 0
305
+
306
+
307
+ if __name__ == "__main__":
308
+ raise SystemExit(main())
@@ -9,10 +9,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
9
9
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
10
10
  metadata:
11
11
  author: PassionCode.ai
12
- version: "0.80.2"
12
+ version: "0.82.0"
13
13
  contract-version: "0.1.0"
14
14
  extension: "fabric-service/0.1"
15
- extension-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
15
+ extension-commit: "93e7db164c7829e2783b4b7705c4113bb5201131"
16
16
  ---
17
17
 
18
18
  # Building Fabric services
@@ -82,7 +82,9 @@ MCP registration into a client config, the login flow or the CSRF header.
82
82
  When other agents or Fabric call the service over MCP, it follows `fabric-interop/0.1`:
83
83
  each manifest capability is the tool of its name, long work is a job with a stable id,
84
84
  a question for a person is an elicitation, and every call carries one trace. A job a limit stops or pauses, and an
85
- effect nobody saw, have their own codes and a key per caller (DEC-0087). Read
85
+ effect nobody saw, have their own codes and a key per caller (DEC-0087); work held by the service's own limits is
86
+ said on the job, pushed to the operator once per hold and lifted by their scoped answer (`scripts/fabric_holds.py`,
87
+ FAA-298). Read
86
88
  [the interop reference](references/interop.md) before writing an MCP tool; the helpers
87
89
  are in [`scripts/fabric_interop.py`](scripts/fabric_interop.py) and
88
90
  [`scripts/fabric-interop.mjs`](scripts/fabric-interop.mjs). When the callers are agents of the estate, the
@@ -308,7 +310,8 @@ reference's *A2A for remote agents* section), `channel-agent.declaration` (DEC-0
308
310
  `board.responder` for a service that declares the project board) and exits 1 on any `FAIL`. A `NOT_RUN` is not a pass;
309
311
  name it in the report. A service with a dashboard also runs the layout probe,
310
312
  `node <skill-dir>/scripts/check_dashboard_layout.mjs --service <id>` (WebKit at 360, 480, 768
311
- and 1280 px; needs Playwright, else `NOT_RUN` and exit 3: nothing was measured).
313
+ and 1280 px; needs Playwright, else `NOT_RUN` and exit 3: nothing was measured; also `NOT_RUN`
314
+ and exit 3 where the socket table is restricted, `table-restricted`, since 0.81.0: FAA-318).
312
315
 
313
316
  Then prove the lock: start a second copy by hand against the same data directory; it
314
317
  must exit 75 while the first keeps serving. Prove recovery: `kill -9` the launchd pid;
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.6 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
1
+ /* fabric-dashboard-kit@1.1.7 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
2
2
  Load it same-origin, after fabric-dashboard.css, with the service it opens Fabric on:
3
3
  <script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
4
4
  data-project="project-a" data-project-name="Project A"></script>
@@ -11,7 +11,7 @@
11
11
  (function (root) {
12
12
  'use strict';
13
13
 
14
- var VERSION = '1.1.6';
14
+ var VERSION = '1.1.7';
15
15
  var COOKIE = 'fabric_button';
16
16
  var GET_FABRIC = 'https://passioncode.ai/fabric';
17
17
  var ABSENT_AFTER_MS = 1500;
@@ -37,8 +37,26 @@
37
37
  session: ['token', 'id'],
38
38
  code: ['login', 'auth', 'access', 'session']
39
39
  };
40
- var JWT_SHAPE = /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*/;
41
- var SECRET_VALUE = /(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|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,})/i;
40
+ // Whether /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*/ matches anywhere, read in one pass (the
41
+ // contract's hasJwt, FAA-313): the expression backtracks from every `eyJ` of a long run, while every `eyJ` of one run
42
+ // shares that run's end, so only its first is tried.
43
+ var URL_BASE64 = /[A-Za-z0-9_-]/;
44
+ function runEnd(text, from) { while (from < text.length && URL_BASE64.test(text.charAt(from))) from += 1; return from; }
45
+ function hasJwt(text) {
46
+ var at = text.indexOf('eyJ');
47
+ while (at >= 0) {
48
+ var one = runEnd(text, at + 3);
49
+ if (one - at - 3 >= 2 && text.charAt(one) === '.') {
50
+ var two = runEnd(text, one + 1);
51
+ if (two - one - 1 >= 2 && text.charAt(two) === '.') return true;
52
+ }
53
+ at = text.indexOf('eyJ', one);
54
+ }
55
+ return false;
56
+ }
57
+ // The contract's SECRET_VALUE at the pin (DEC-0151), as the kit's fabric-secrets.mjs writes it (FAA-314, kit 1.1.7):
58
+ // `Bearer ` only at a value's start, a provider key's prefix in its canonical case after a boundary.
59
+ var SECRET_VALUE = /(^[Bb][Ee][Aa][Rr][Ee][Rr]\s.+|(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt"']|\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)(?:sk-(?=[A-Za-z0-9_-]{0,255}?\d)(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})|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,}))/;
42
60
  var LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
43
61
 
44
62
  function nameWords(name) {
@@ -60,7 +78,7 @@
60
78
  function longSecret(value) {
61
79
  return (value.match(LONG_RUN) || []).some(function (run) { return /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run); });
62
80
  }
63
- function secretValue(value) { return JWT_SHAPE.test(value) || SECRET_VALUE.test(value) || longSecret(value); }
81
+ function secretValue(value) { return hasJwt(value) || SECRET_VALUE.test(value) || longSecret(value); }
64
82
  function decodeKeepingPlus(raw) { try { return decodeURIComponent(raw); } catch (e) { return raw; } }
65
83
  function firstKey(text) { var it = new URLSearchParams(text).keys().next(); return it.done ? '' : it.value; }
66
84
  function rawValues(query) {
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.6 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.7 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
3
3
  reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
4
4
  Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.6 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.7 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  Load it same-origin with <script src="fabric-dashboard.js" defer></script>: no inline script, no
3
3
  eval, nothing from a CDN. It applies the host's appearance (ED-6, ED-8), folds navigation that
4
4
  does not fit into a More menu (ED-3), makes wide boxes keyboard-reachable (ED-4) and keeps
@@ -8,7 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  var KIT = 'fabric-dashboard-kit';
11
- var VERSION = '1.1.6';
11
+ var VERSION = '1.1.7';
12
12
  /* fabric-dashboard-kit:theme-ids:begin */
13
13
  var THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
14
14
  /* fabric-dashboard-kit:theme-ids:end */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabric-dashboard-kit",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "profile": "fabric-dashboard/0.1",
5
5
  "decision": "DEC-0036",
6
6
  "declarationKey": "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1",
@@ -15,9 +15,9 @@
15
15
  "contrast"
16
16
  ],
17
17
  "files": {
18
- "fabric-dashboard.css": "7a90bbe1f1a2170df5dac0f596a3f302167bef9a7243457bb21dd0eb34f4d359",
19
- "fabric-dashboard.js": "146ec9a0fd50af2df3245c02ec7dfba75b7d4e8cb5476fedb5ef727869fef1cc",
20
- "fabric-button.js": "60c2b6d44571213e0237c29a94a4cf220f9de3efcfa697ac6a33777d1ad25fd6",
18
+ "fabric-dashboard.css": "549c1c1546bc9f8d2c1a579e7ad1035d009c94c3324b91d89d892afc437d5800",
19
+ "fabric-dashboard.js": "fa966a8f3b228391c2dae33674887a0d44b102cb58e42dbe6331f08bab57b391",
20
+ "fabric-button.js": "220c5c9719a9f6e35851d8748e2abb1d6606ffdb9fc43f3aff2b01f34637d925",
21
21
  "themes.json": "511839f1a1a83eb37fadfd132791474497d5c28c5091700b9aa31ad076e359b5"
22
22
  },
23
23
  "history": {
@@ -59,6 +59,11 @@
59
59
  "fabric-dashboard.css": "7a90bbe1f1a2170df5dac0f596a3f302167bef9a7243457bb21dd0eb34f4d359",
60
60
  "fabric-dashboard.js": "146ec9a0fd50af2df3245c02ec7dfba75b7d4e8cb5476fedb5ef727869fef1cc",
61
61
  "fabric-button.js": "60c2b6d44571213e0237c29a94a4cf220f9de3efcfa697ac6a33777d1ad25fd6"
62
+ },
63
+ "1.1.7": {
64
+ "fabric-dashboard.css": "549c1c1546bc9f8d2c1a579e7ad1035d009c94c3324b91d89d892afc437d5800",
65
+ "fabric-dashboard.js": "fa966a8f3b228391c2dae33674887a0d44b102cb58e42dbe6331f08bab57b391",
66
+ "fabric-button.js": "220c5c9719a9f6e35851d8748e2abb1d6606ffdb9fc43f3aff2b01f34637d925"
62
67
  }
63
68
  }
64
69
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  A service answers one inbound question from its own knowledge file, or hands it to a person. It is a step of an
4
4
  agent that answers people, not that agent: see the paragraph after the table. Under the creating-fabric-agents
5
- template (`communication-agent-template.md`) the plan-and-draft call writes the draft, not this recipe's `answer()`:
5
+ template (`templates/communication-agent/`, read through `references/communication-agent-template.md`) the plan-and-draft call writes the draft, not this recipe's `answer()`:
6
6
  the draft carries a `[KB:<id>]` marker for each knowledge id the plan used, and the agent runs `check_reply(draft,
7
7
  question, prompt_ids, max_chars=..., sources=...)` on it for the code checks. `prompt_ids` are the ids of the items
8
8
  placed in the plan's prompt, never the plan's own list (an id the plan names outside them is held as a suspected
@@ -37,7 +37,8 @@ refuses it with `invalid_arguments` (FAA-244).
37
37
  binding credential, shown once, goes into `credential_file` (0600, atomic) and never into the answer, a log or argv.
38
38
  Keep the file in the service's data directory or the operator's vault.
39
39
  3. Each connection joins with `fabric.board.join {projectId}` (`join_board` / `joinBoard`). It re-reads `hub.json`,
40
- checks the pid it names is alive, tells the owner of the door's listener from the socket table (DEC-0154 HA-2:
40
+ checks the pid it names is alive (a process of another OS account under that id is `HubError` code `not-a-hub`,
41
+ retryable, saying not verified, as the session hooks say it, FAA-304; none at all is `not-running`), tells the owner of the door's listener from the socket table (DEC-0154 HA-2:
41
42
  another OS account's process, or one whose owner cannot be told, is `HubError` code `foreign-user`, retryable, with
42
43
  nothing sent; a restricted macOS table, which a retry never clears, is `data.reason` `table-restricted` and not
43
44
  retryable, protocol.md, FAA-272), and only then sends the credential (a session already running carries that refusal as `BoardError` code
@@ -403,7 +404,8 @@ Python (`fabric_hub`): `fabric_root(platform, env)`, `locate_hub(root=None, aliv
403
404
  `read_door_token(location)`, `read_credential(path)`, `HubClient(root=None, *, alive, timeout, estate, post)` with
404
405
  `request_board_access(agent_id, project_id, capabilities, reason)`, `poll_access(request_id, poll_secret,
405
406
  credential_file)`, `join_board(project_id, credential_file) -> BoardGrant`, `call_door(tool, args)`,
406
- `call_binding(tool, args, credential_file)`; `Backoff(first=2, cap=300, jitter=0.2, floor=1)`; `HubError(code,
407
+ `call_binding(tool, args, credential_file)`; `pid_alive(pid)` answers True, False, or None for another OS account's
408
+ process (not verified); `Backoff(first=2, cap=300, jitter=0.2, floor=1)`; `HubError(code,
407
409
  message, retryable, data)`.
408
410
 
409
411
  Python (`fabric_board`): `Responder(*, hub, project_id, capabilities, credential_file, checkpoint_file, handler,
@@ -105,7 +105,8 @@ service. Load it after the stylesheet, with ids only:
105
105
  Fabric* on a Russian page, or the page's `data-label-open`);
106
106
  `--ink` on `--panel` with an `--edge` boundary, the pairs the themes are measured on.
107
107
  - **Links (FL-1…FL-6):** built with the kit's port of the contract's `buildFabricLink`; a value outside the grammar
108
- (or one shaped like a credential) means no button, never a bad link. A press opens a view in Fabric and starts
108
+ (or one shaped like a credential, by the contract's DEC-0151 detector as the kit's `fabric-secrets.mjs` reads it, since
109
+ 1.1.7) means no button, never a bad link. A press opens a view in Fabric and starts
109
110
  nothing (FB-8).
110
111
  - **The host (FB-6):** when the host's `fabric_button` cookie says `owner=host`, the page hides its own button.
111
112
  - **Fabric absent (FB-5):** when the cookie says `fabric=absent`, or `data-fabric="absent"`, or when the page is still
@@ -148,6 +149,16 @@ vertically by the viewport or a clipping ancestor; ED-4 is `NOT_RUN` on a page p
148
149
  the scroll position: say so in the report. A `NOT_RUN` is not a pass. Exit codes: 0 when no rule fails and at least
149
150
  one passed, 1 on a `FAIL`, 2 on a usage error, 3 when no rule was measured (every rule `NOT_RUN`, since 0.44.0).
150
151
 
152
+ Since 0.81.0 the probe opens a loopback page (and asks a local service for its login code) only after it reads the
153
+ page's listener from the socket table as this OS account's (DEC-0154 PL-3, HA-2; FAA-260). On a Mac whose socket
154
+ table is restricted for the runtime that runs the probe (macOS 27.2 shows an ad-hoc-signed Node, such as Homebrew's,
155
+ nothing through `netstat`; DEC-0155 RT-1) the listener reads `unknown` (`table-restricted`), so every rule is
156
+ `NOT_RUN` and the probe exits 3, whatever the page. That is not a pass and not the page's fault: say so in the report,
157
+ with the reason the probe printed. The probe does not vouch for the listener by the service's pid, since only a
158
+ caller that started the process may (DEC-0155 RT-3, the installers' route), and the well-known `process.pid` proves
159
+ nothing. To measure the page, run the probe with a Node whose table reads (an Apple-signed or Developer ID one), or
160
+ on a host without the restriction (FAA-318).
161
+
151
162
  ## Upgrading an existing dashboard
152
163
 
153
164
  A session that starts (or is cleared) in a repository that owns a service, whose dashboard has no
@@ -166,7 +177,9 @@ In the middle of an unrelated task, finish that first or record the upgrade as a
166
177
  3. A page with an older kit: `dashboard_kit.py install <dir>`. A `changed` copy is refused; move the
167
178
  edits into the page's own files, then install (or `--force` when the operator says the edits go).
168
179
  4. Raise `kit` in the declaration (`dashboard_extension()` does it), restart the service, and run
169
- both probes above. Report the layout table with every `NOT_RUN`.
180
+ both probes above. Report the layout table with every `NOT_RUN`. On a Mac with a restricted socket
181
+ table the layout probe measures nothing since 0.81.0 (every rule `NOT_RUN`, exit 3, the reason
182
+ `table-restricted`; *Proving it* above): report that as not measured, never as a pass.
170
183
 
171
184
  ## When something is missing
172
185
 
@@ -65,8 +65,8 @@ host's `open` with `fallback=never`. Never `open <url>`, `open -a Safari`,
65
65
  agent's pages. A browser only when `host_status` confirms `not_installed`, and
66
66
  a raw `http://127.0.0.1:<port>` address only after the listener on that port is
67
67
  this OS account's (DEC-0154 PL-3 and HA-2; the adapter's `building-fabric-services/scripts/fabric_peer_owner.py`: `server_end` of a connection it opens and sends nothing on; `check_service.py <id>.<instance>` prints it as `listener.owner`): a
68
- `foreign-user` listener is reported, never opened (the link gate below does not
69
- read the owner yet, FAA-251). An installed host that failed
68
+ `foreign-user` listener is reported, never opened (the link gate below takes
69
+ that owner as `listener_owner`). An installed host that failed
70
70
  to open is reported, not replaced by HTTP. Links
71
71
  carry no token or login code. Every completion and notification action passes
72
72
  the Fabric Agent Adapter's link gate before delivery: `check_dashboard_link.py`
@@ -119,7 +119,20 @@ Its trusted context is supplied by the host, not by untrusted model output:
119
119
 
120
120
  `native_link` must equal the resolved open_link, with an available host on the
121
121
  viewer's device. `browser_link` must equal http_url, with confirmed not_installed,
122
- if_absent policy and the same device. Contract DEC-0123 PL-3 also allows a browser
122
+ if_absent policy, the same device and `"listener_owner": "this-account"`: the kind
123
+ the host read for the listener on http_url's port (`fabric_peer_owner`'s
124
+ `this-account`, `another-account`, `system`, `unknown`, `none`, or
125
+ `check_service.py`'s `foreign-user`); any other, or none given, refuses the
126
+ browser (DEC-0154 PL-3, FAA-251). The host reads that kind right before it opens
127
+ the page with the adapter's
128
+ `python3 building-fabric-services/scripts/fabric_peer_owner.py listener <port>`
129
+ (or `node building-fabric-services/scripts/fabric-peer-owner.mjs listener <port>`),
130
+ which prints it on one line (`--json` prints the whole owner) and sends nothing
131
+ to the port (FAA-315). Where the socket table is restricted (macOS 27.2 for an
132
+ ad-hoc-signed Python or Node, DEC-0155 RT-4) only this account's listener on
133
+ exactly `127.0.0.1` reads `this-account`; a wildcard or `::1` bind reads
134
+ `unknown` (`table-restricted`), and the browser is refused. The native link does not take it: Fabric
135
+ Dashboards checks its own listener. Contract DEC-0123 PL-3 also allows a browser
123
136
  on `unsupported` (a system the host does not ship for); the gate treats
124
137
  `unsupported` as no fallback, by the operator's rule that a browser opens only on a
125
138
  confirmed `not_installed`, and macOS never reports it (FAA-137). `remote_open` instead contains `device_id`,
@@ -53,8 +53,11 @@ the same owner; **T2** everyone else, and any call relayed onto loopback from of
53
53
  hub forwarding a T2 caller). DL-26: a T0 or T1 caller is never required to present a token; pass the tier,
54
54
  `gate(tool, headers, tier="T0")` or `server.handle(message, headers, tier="T0")`, and the call passes with no
55
55
  delegation. On a computer with more than one counted OS account, a loopback caller with no token is T0 only after the
56
- door checks the peer's account (the pinned DEC-0154 HA-3 and DL-25; the kit's own peer check is FAA-227), so pass
57
- `tier="T0"` there only for a caller the descriptor token authenticated. A token such a caller presents is still verified and refused when invalid. A T2 caller takes the whole
56
+ door checks the peer's account (the pinned DEC-0154 HA-3 and DL-25): the kit's `LoopbackHTTPServer` with `peer_check="strict"` (Node
57
+ `loopbackServer(app, {peerCheck: 'strict'})`, [protocol.md](protocol.md#the-listeners-owner-dec-0154-ha-2)) is that check,
58
+ for loopback peers: pass `tier="T0"` only when the handler's `self.server.peer_owner()` (`connectionOwner(req.socket)`)
59
+ names an owner, since a peer off loopback is served without one and is T2;
60
+ otherwise pass `tier="T0"` there only for a caller the descriptor token authenticated. A token such a caller presents is still verified and refused when invalid. A T2 caller takes the whole
58
61
  profile. Money limits and human gates bind at every tier.
59
62
 
60
63
  **The family and the agent network** (DEC-0130, wording settled by DEC-0137). The operator's family is every agent
@@ -159,6 +159,33 @@ node keys every per-caller rule with `job_caller_key`, counts every started job
159
159
  and paused ones too) as running, and answers another caller's job id `unknown-job`. The text-only `queue-full` result
160
160
  and `fabric.job.list` are still the node's own (FAA-103).
161
161
 
162
+ ### Work held by the service's own limits
163
+
164
+ A service that holds work by a limit of its own (a daily quota, a per-caller share, a concurrency cap, a money cap)
165
+ says so, so a hold is never seen only by a caller who waits (FAA-298). `fabric_holds` (Node `fabric-holds.mjs`) does
166
+ the three parts:
167
+
168
+ - **On the job.** `hold_job(jobs, job_id, reason, since, until, message)` (Node `holdJob`) pauses the job with the
169
+ statusMessage `waiting_message` writes: `quota-exhausted[ until <until>]: waiting <reason> since <since>[: <message>]`,
170
+ `reason` one of `daily-quota`, `caller-share`, `concurrency`, `money`. The pinned `interop-job` schema is closed
171
+ (`additionalProperties: false`), so `waiting {reason, since, until, message}` is carried in `statusMessage`, the
172
+ contract's pause grammar, never as a field of the job: every caller already reads it as wait (until `until` when
173
+ given), and `read_waiting(answer)` (Node `readWaiting`) gives the four parts back. A job waiting its normal turn in
174
+ the call queue keeps `queued <position> of <length>`; a hold is the service's own limit.
175
+ - **One push per hold.** `HoldBook(path, events=log)` (Node `new HoldBook(file, {events})`) opens a hold per reason and
176
+ caller: `hold(caller=, work=, reason=, until=, job_id=)` returns `{hold, statusMessage, push}`, and only the call that
177
+ opens the hold pushes: one `limit.held` event, level `notice`, `notify: true`, naming the caller, the work, the reason
178
+ and when it frees by itself, and asking whether to raise the limit. More work held by the same limit for the same
179
+ caller joins the hold silently. A hold closes by `release`, when its `until` passes, or by a lift that covers it;
180
+ the next hold pushes again. Names that read as a credential are refused.
181
+ - **The operator's answer as a scoped lift.** `lift(reason=, decision=, caller=, job_id=, jobs= | usd= | limit=,
182
+ until=)` records the operator's words (`fabric_actions.operator_decision("limit.lift", words)`) with its scope (this
183
+ order, one caller, or every caller) and its size: a number of jobs, an amount of money, or a raised limit for a window
184
+ (100 a day for today: `limit=100, until=<midnight>`), and writes `limit.lifted`. `admit(reason=, caller=, job_id=,
185
+ cost_usd=)` spends a jobs or money lift (the narrowest scope first) and answers whether the work may pass the limit;
186
+ pass its answer to `jobs.create(..., limit_holds=not admitted)` for a repeated key. `limit(reason=, base=, caller=)`
187
+ is the limit in force. A lift falls back by itself when its window ends or its amount is spent.
188
+
162
189
  <a id="actions"></a>
163
190
 
164
191
  ## Actions
@@ -412,5 +439,6 @@ your own record, keep it (OA-6).
412
439
  | `interop.events-trace` | every event with a trace carries both `traceId` and `spanId`, well formed | no events page was read |
413
440
  | `interop.action-tools` | every action the well-known `extensions/actions/0.1` declares names a tool `tools/list` serves (DEC-0121 AP-1); one it does not serve is a `FAIL`, and so is a local service that declares actions with no MCP surface (beyond FAC-SEM-142, which reads only a present list) | no declaration, an unreadable one (`well-known.actions` reports it), no listing was read, or a remote service with no MCP surface (its twin may be on A2A or behind a gateway) |
414
441
  | `channel-agent.declaration` | a channel agent's declaration (DEC-0150) passes the contract's FAC-SEM-162 to 165 at the pin, read from `provider.extensions` of the manifest the descriptor names, else from `extensions` of the well-known answer (CA-1); a `FAIL` on the obligations of a declaration the reader still reads (FAC-SEM-164) or a string shaped like a credential (FAC-SEM-165, not a declaration too large to read for credentials) in the read declaration or in any copy beside it, the first five findings shown as code and path, never a declared value | nothing declared, the named manifest unreadable (`interop.manifest-link` fails it), or a key read as not declared (a manifest's own `extensions`, a capability's, another version's key, a well-known copy beside a manifest, a form the schema or FAC-SEM-163 refuses), each named in the evidence, never a failure while producers move to this revision unless a copy holds a string shaped like a credential (that fails in any copy the probe reads: a manifest's own `extensions`, a capability's, the well-known copy, CA-5); no line when the well-known answer is not read; credentials are read as the contract reads them, within its reading budget and under the `0.1` key only (a key of another version is named, not read); a well-known answer carrying the declaration at 75 % of the 2 MiB cap or over is named, never a failure (until the contract's CT-223) |
442
+ | `limits.holds` | a service that declares limits (a load declaration's `limits`, the usage report's `budget` or `budgets`) holds work through `fabric_holds` (`HoldBook`, `waiting_message`, `hold_job`) in a tracked code file (`.py`, `.mjs`, `.js`, `.ts`) of its checkout outside the kit's copy, a vendored `check_service_rules.py` and the tests (FAA-298) | no limits declared, or no checkout found (`--repo` names it), git missing or out of time; and, since 0.81.0, a service that declares limits and holds none through the kit: a migration note naming `fabric_holds`, never a `FAIL`, since services built before the module could not have adopted it |
415
443
 
416
444
  `python3 scripts/check_service.py <id>` prints them with the rest; `--json` for a machine.