@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.
- package/CHANGELOG.md +157 -0
- package/README.md +18 -5
- package/SECURITY.md +25 -8
- package/bin/fabric-agent-adapter.js +24 -1
- 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/dashboard-links.md +16 -3
- 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 +116 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +33 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +257 -10
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -77
- 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 +899 -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 +7 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +23 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- 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/board.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +47 -10
- 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 +31 -8
- 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 +33 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +19 -28
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +9 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +146 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +79 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +269 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +64 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +73 -3
- 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 +89 -22
- 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 +39 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +76 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +8 -153
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +86 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +324 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +34 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +35 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +77 -4
- 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 +156 -133
- 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 +257 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +64 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +246 -24
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +16 -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/communication-agent-template.md +19 -328
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +16 -3
- 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/source-and-instance.md +44 -7
- 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 +33 -3
- 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 +2 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +16 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +46 -31
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +18 -16
- 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.
|
|
12
|
+
version: "0.82.0"
|
|
13
13
|
contract-version: "0.1.0"
|
|
14
14
|
extension: "fabric-service/0.1"
|
|
15
|
-
extension-commit: "
|
|
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)
|
|
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.
|
|
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.
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 */
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-dashboard-kit",
|
|
3
|
-
"version": "1.1.
|
|
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": "
|
|
19
|
-
"fabric-dashboard.js": "
|
|
20
|
-
"fabric-button.js": "
|
|
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
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md
CHANGED
|
@@ -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
|
|
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)`; `
|
|
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,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md
CHANGED
|
@@ -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
|
|
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
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md
CHANGED
|
@@ -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
|
|
69
|
-
|
|
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
|
|
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`,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md
CHANGED
|
@@ -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
|
|
57
|
-
`
|
|
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.
|