@passioncode-ai/fabric-agent-adapter 0.61.0 → 0.62.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 +104 -0
- 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 +4 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +11 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +19 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +6 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +33 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +33 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +16 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +55 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +102 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +42 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,109 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.62.0 (2026-11-04)
|
|
4
|
+
|
|
5
|
+
A new install names the repository of its code, and a service says what it uses, so a map of the installed agents can
|
|
6
|
+
link each to its project and draw its chain (FAA-198, FAA-199).
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- The reference service declares its connections (`extensions/connections/0.1`): the template it drafts from and the
|
|
11
|
+
demo source `serve --degraded` marks failing; the online example (`sample-remote-service.mjs`) declares an empty list,
|
|
12
|
+
since it calls nothing outside itself. The support-agent recipe's `SupportAgent.connections()` names its two models
|
|
13
|
+
for the service that runs it, keeping only an id the connections rule accepts, and imports `fabric_connections` only
|
|
14
|
+
when called (the recipe's install line names the module). `check_service.py` reports `well-known.connections` as `NOT_RUN` when the
|
|
15
|
+
well-known document has no connections declaration (it reported nothing before); an empty list is one and reads
|
|
16
|
+
`PASS`, and adapting-projects-to-fabric names the declaration in its
|
|
17
|
+
adapter step (FAA-199).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `write_descriptor` / `writeDescriptor` refuse a new descriptor that names no `source.repository` (no descriptor of its
|
|
22
|
+
`id.instance` is installed yet: an installed one is a regular file, whatever it holds, so a folder, a broken link or
|
|
23
|
+
a path that cannot be examined is none), with `ServiceError` naming the descriptor; `register_remote` / `registerRemote` refuse it before they
|
|
24
|
+
write the token file, so a refused registration leaves nothing, and a rotation that names no `source` keeps the
|
|
25
|
+
installed one when the kit accepts it (a hand-edited one it refuses is dropped, as before). Rewriting a descriptor already installed without it still writes it and warns
|
|
26
|
+
(`NoSourceRepositoryWarning`, a `UserWarning` attributed to the installer's call; Node `process.emitWarning`, code
|
|
27
|
+
`FABRIC_NO_SOURCE_REPOSITORY`), so an installed agent's update keeps working. `source_refusal` / `sourceRefusal` say
|
|
28
|
+
why an install would be refused, for an installer that checks before it validates or writes anything else (a `source`
|
|
29
|
+
that is no object names nothing), and `names_repository` / `namesRepository` whether a descriptor names one.
|
|
30
|
+
`check_service.py` names a descriptor without it, or with one the descriptor's rules refuse, and why
|
|
31
|
+
(`descriptor.source-repository`, `NOT_RUN`, never a pass), and prints a repository without its userinfo, query or
|
|
32
|
+
fragment (a URL through `redact_url`; a local path, one that starts with `/`, `~`, `.`, `\\` or a drive, by its name;
|
|
33
|
+
a value of a URL scheme such as `https:` also without `//`, through `redact_url`; an scp form or any other value from
|
|
34
|
+
after its user and password, whatever they hold; one with a piece shaped like a credential that is no name not at
|
|
35
|
+
all), as
|
|
36
|
+
`state.outside-code` and `dashboard.opens-in-host` now do too for the source and for the data checkout's origin (they
|
|
37
|
+
printed them as found).
|
|
38
|
+
`sample_service.py register` writes this repository, whose code its descriptor runs whatever `--id` names, takes
|
|
39
|
+
`--repository`, and writes its manifest only after the descriptor, so a refusal of any kind leaves none naming it, and
|
|
40
|
+
a manifest it cannot write takes a new descriptor back with it, or puts an installed one's bytes back; an installed
|
|
41
|
+
descriptor it cannot read is never replaced (it refuses before writing anything). No owner field is added: the
|
|
42
|
+
organization follows from the repository (FAA-198). `source.repository` is read for a credential by one rule in both kits and
|
|
43
|
+
in the provider entry's check (`repository_credential` / `repositoryCredential` in `fabric_secrets`): its user, its query
|
|
44
|
+
and fragment as `secret_url` reads them (a parameter named for a credential, such as `access_token`, now counts), and
|
|
45
|
+
a path segment shaped like a key; a path segment in lower case, or of letters with no digit, is the repository's
|
|
46
|
+
name unless a key's shape (such as `sk-`, `ghp_`, `xoxb-`, `glpat-`, `AKIA`, `eyJ`) starts it or follows a `.`,
|
|
47
|
+
`_` or `-` in it, so `task-pipeline-dashboard` and `Task-Pipeline-Dashboard`, which read as an `sk-` key, are accepted (a source with it was refused before). A user is accepted only in a URL's
|
|
48
|
+
authority (`https://user@host/path`): an scp form or an opaque URI holding an `@` (`bot:...@host:path`,
|
|
49
|
+
`https:user@host/path`) is refused, and so is a password in the user information read any way a reader takes it:
|
|
50
|
+
as written (`https://h\u:pw@x/r`, as git reads it) or as the URL standard does (`https:///u:pw@h/r`). Node reads
|
|
51
|
+
the user information and parameters by hand, as Python does, so a special scheme written without `//`
|
|
52
|
+
(`https:host/r?access_token=...`) or a host `new URL` refuses is read alike.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- `redact_url` / `redactUrl` (the URL an `HttpError` carries and `check_url` / `checkUrl` print) dropped a user
|
|
57
|
+
information only as the URL standard reads it, so `https://h\u:pw@x/r`, which git and Python's `urllib` read as a
|
|
58
|
+
password, printed `pw` in its path in both kits (0.61.0 too); they now drop the user information as
|
|
59
|
+
written first, after any run of slashes or backslashes that follows the scheme (`https:/h\u:pw@x/r`,
|
|
60
|
+
`https:\\h\u:pw@x/r`; 0.62.0 seam readings 11 and 13).
|
|
61
|
+
- Node's `checkUrl` sent `ht<tab>tps://h\u:pw@x/r` and `https:\/h\u:pw@x/r`, which Python's `check_url` refuses
|
|
62
|
+
(since 0.60.0): it now reads the written user information on the text `new URL` reads, after any run of slashes.
|
|
63
|
+
- Node's `redactUrl` printed `<unparseable url>` for a URL of a scheme other than http, https, ws, wss or ftp (`file:`
|
|
64
|
+
included) that `new URL` cannot parse (`ssh://[zz]/r`, `ssh://h:99999/r`, `file://h:1/r`), where Python keeps the
|
|
65
|
+
scheme: both now print `<scheme>:<redacted>`.
|
|
66
|
+
- A subscription `runner.kind` that is no string (a list or an object) raised `TypeError` in Python's
|
|
67
|
+
`validate_descriptor`, `write_descriptor` and `check_service.py` (0.61.0 too), and Node, for a list whose text is a
|
|
68
|
+
subscription runner's name (`["claude-code"]`, which `Object.hasOwn` reads as that key), named only one of its two
|
|
69
|
+
problems: both kits now name both as descriptor problems.
|
|
70
|
+
- Node's key set cache (`JwksCache`) accepted `https://h\u:pw@x/j` and `https://@x/j`, which the Python twin refuses:
|
|
71
|
+
it now reads the authority as written too.
|
|
72
|
+
|
|
73
|
+
### Upgrading
|
|
74
|
+
|
|
75
|
+
- An installer that writes a descriptor for a new install (`register_remote` / `registerRemote` included) and a desktop
|
|
76
|
+
app that writes its own (DEC-0069) pass `source: {"repository": "https://github.com/owner/name"}` (the repository
|
|
77
|
+
their code is built from) in the call that writes it: a source set by a later write is too late, since the first call
|
|
78
|
+
is refused before anything is written. An installer that loads the kit from the installed plugin meets this as
|
|
79
|
+
soon as the plugin updates; an installed agent's next update warns until it names one. Only a rewrite in
|
|
80
|
+
place keeps working: an update that removes the descriptor before it writes it again, a new instance (a preview) and a
|
|
81
|
+
moved services directory are new installs. In Python the warning is `NoSourceRepositoryWarning` (a `UserWarning`), so
|
|
82
|
+
a suite that runs with warnings as errors (`-W error`) sees the rewrite raise; filter that class there.
|
|
83
|
+
- Once a descriptor names its repository, two probe rules that read `NOT_RUN` without it start judging:
|
|
84
|
+
`state.outside-code` fails data kept inside that checkout, and `dashboard.opens-in-host` reads the checkout for pages
|
|
85
|
+
opened in a browser. A service that passed may fail them until its data moves out of the checkout or its pages open
|
|
86
|
+
through Fabric Dashboards.
|
|
87
|
+
- Declare what the service uses with `connections_extension` / `connectionsExtension` (an empty list when it uses
|
|
88
|
+
nothing): until it does, `well-known.connections` reads `NOT_RUN` and the connection map has no row for it.
|
|
89
|
+
- A `source.repository` (a service's or a provider entry's) that names a user outside a URL's authority (an scp form,
|
|
90
|
+
`https:user@host/path`) or carries a query parameter named for a credential is refused: write it as
|
|
91
|
+
`https://github.com/owner/name`, with no user, query or fragment. A path segment that mixes upper case with a digit
|
|
92
|
+
is read for a key's shape (`sk-` and 16 more characters, a `ghp_` run, 40 characters of mixed case and digits), so
|
|
93
|
+
`Task-Pipeline-Dashboard2` is refused while `Agent-V2` is not: write the repository's name in lower case (hosts such
|
|
94
|
+
as GitHub take either). A segment that a key's shape starts, or follows a `.`, `_` or `-` in, is refused whatever
|
|
95
|
+
its case and also percent-encoded (`%73k-` reads as `sk-`). The shapes are those of `fabric_secrets.SECRET_VALUE`,
|
|
96
|
+
such as `sk-` and 16 more characters, `gh` and one of p, o, u, s, r then `_` and 20, `xox` and one of a, b, o, p, r,
|
|
97
|
+
s then `-` and 10 (so the word `xoxo-` counts), `glpat-` and 20, `AKIA` and 16, and a JWT as written (`eyJ` in
|
|
98
|
+
that case, `JWT_SHAPE`); `bearer` and a space counts at a segment's start once decoded (`Bearer%20Tracking` is
|
|
99
|
+
refused, `team-Bearer%20Tracking` stays a name), and anywhere in a segment or a host when 12 or more token
|
|
100
|
+
characters with a digit follow it and no letter, digit or `_` comes before it (`x-Bearer%20<a token>` is refused).
|
|
101
|
+
A host is read for a key's shape anywhere in it, inside a word too, so `task-pipeline-dashboard.example.com` is
|
|
102
|
+
refused although the same path segment is a name (FAA-200 (32)). Such a host, like a repository such
|
|
103
|
+
as `sk-learn-` followed by a longer name, has no spelling the kit accepts, so an agent built from it is not installed
|
|
104
|
+
new, and
|
|
105
|
+
an installed one keeps working by a rewrite in place that leaves the source out (FAA-200 (24)).
|
|
106
|
+
|
|
3
107
|
## 0.61.0 (2026-11-04)
|
|
4
108
|
|
|
5
109
|
The board responder reads the operator's mandate from a request of his own Project (FAA-135), and working-with-fabric
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.62.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.62.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
11
11
|
---
|
|
@@ -143,6 +143,9 @@ Preserve the chosen ownership boundary:
|
|
|
143
143
|
`building-fabric-services`'s `scripts/fabric_interop.py`.
|
|
144
144
|
- Local runner maps typed input, executable identity, result location, cancellation,
|
|
145
145
|
heartbeat, and partial results without relying on ambient accounts.
|
|
146
|
+
- A service declares what it uses (`extensions/connections/0.1`: models, other agents, connectors, stores,
|
|
147
|
+
channels) with `connections_extension` / `connectionsExtension`, and its descriptor names `source.repository`;
|
|
148
|
+
the probe names either one missing (FAA-198, FAA-199).
|
|
146
149
|
|
|
147
150
|
Keep model selection and provider-internal reasoning outside the Fabric contract. Expose
|
|
148
151
|
typed outcomes, evidence, artifacts, and protocol-visible state, not chain-of-thought.
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md
CHANGED
|
@@ -57,8 +57,10 @@ It refuses, with one sentence that never quotes a value:
|
|
|
57
57
|
(`test_provider.SchemaParityTests` compares them with a contract checkout at the pin when `FABRIC_CONTRACT_NEW` names
|
|
58
58
|
one, as the contract job of the nightly CI does), and the reader is held to every `provider.schema.json` fixture of
|
|
59
59
|
the pin (`test/fixtures/contract/provider-fixtures.json`, `test_provider.ContractFixtureTests`);
|
|
60
|
-
- a `source.repository` with a `user:password
|
|
61
|
-
|
|
60
|
+
- a `source.repository` with a `user:password@`, a user outside a URL's authority, or a value shaped like a credential
|
|
61
|
+
in its user, query or fragment or as a path segment (one in lower case, or of letters with no digit, is the name unless a key's shape starts it or follows a `.`, `_` or `-`, read as written and percent-decoded; a host is read for a key's shape anywhere in it,
|
|
62
|
+
inside a word too, so `task-pipeline-dashboard.example.com` is refused, FAA-200 (32)): it names the
|
|
63
|
+
repository, never reaches it (since 0.49.3; one rule with the service kit's descriptor check since 0.62.0);
|
|
62
64
|
- a manifest whose name follows a backslash (`C:\Agents\example\fabric-agent.json`). The reader,
|
|
63
65
|
`validate_provider_entry`, accepts it since DEC-0145 (`MANIFEST_PATTERN`, the schema's), but a reader that has not
|
|
64
66
|
re-pinned to DEC-0145 refuses the entry, Fabric's registry among them (CT-192), so the writer does not emit it until
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py
CHANGED
|
@@ -147,7 +147,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
|
|
|
147
147
|
problems.append("source is {repository?}")
|
|
148
148
|
elif "repository" in source and not _is_uri(source["repository"]):
|
|
149
149
|
problems.append("source.repository is an absolute URI")
|
|
150
|
-
elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.
|
|
150
|
+
elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.repository_credential(source["repository"])):
|
|
151
151
|
# FAA-120, as the service kit: a repository is named, never reached with a credential in its URI.
|
|
152
152
|
problems.append("source.repository carries no password or credential: it names the repository")
|
|
153
153
|
if not isinstance(entry["installedAt"], str) or not _TIMESTAMP.fullmatch(entry["installedAt"]):
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py
CHANGED
|
@@ -185,3 +185,60 @@ def secret_url(value: str) -> bool:
|
|
|
185
185
|
if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
|
|
186
186
|
return True
|
|
187
187
|
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
_REPOSITORY_URI = re.compile(r"\A([A-Za-z][A-Za-z0-9+.-]*:(?://[^/?#]*)?)([^?#]*)(.*)\Z", re.S)
|
|
191
|
+
_REPOSITORY_NAME = re.compile(r"(?:[a-z0-9._-]+|[A-Za-z._-]+)\Z") # lower case, or letters with no digit (readings 8)
|
|
192
|
+
_REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
|
|
193
|
+
_KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % SECRET_VALUE.pattern, re.IGNORECASE | re.ASCII)
|
|
194
|
+
_JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def repository_name(piece: str) -> bool:
|
|
198
|
+
"""Whether a path segment reads as a repository's name: lower case, or letters with no digit, with no provider
|
|
199
|
+
key's or JWT's shape (`sk-`, `ghp_`, `xoxb-`, `AKIA`, `eyJ`) at its start or after a `.`, `_` or `-` (0.62.0
|
|
200
|
+
readings 9 and 10), so a letters-only key is still a key while `task-pipeline-dashboard`, which holds an `sk-` shape
|
|
201
|
+
inside a word, is a name."""
|
|
202
|
+
return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not _JWT_AT_A_WORD.search(piece)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def ascii_decoded(text: str) -> str:
|
|
206
|
+
"""Every `%XX` escape of an ASCII character decoded, the rest left as written: how a repository's path and user
|
|
207
|
+
information are read as well as written, since git and hosts decode them (`%73k-` is `sk-`, `u%3Apw` is `u:pw`;
|
|
208
|
+
0.62.0 readings 11). The Node twin is asciiDecoded."""
|
|
209
|
+
return re.sub(r"%([0-7][0-9A-Fa-f])", lambda escape: chr(int(escape.group(1), 16)), text)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def repository_password(value: str) -> bool:
|
|
213
|
+
"""Whether a repository URI's user information holds a password, in any scheme, `file` too: every slash and
|
|
214
|
+
backslash after the colon skipped, then up to the next `/`, `?` or `#`, a backslash kept in the user. That one
|
|
215
|
+
reading holds git's (`https://h\\u:pw@x/r`), curl's (`https:///h\\u:pw@x`) and the URL standard's
|
|
216
|
+
(`https:///u:pw@h`, which stops at a backslash, a prefix of it) (0.62.0 readings 10 to 14). The user information
|
|
217
|
+
ends at the last `@`; a password follows its first `:`, as written or ascii_decoded. The Node twin is
|
|
218
|
+
repositoryPassword."""
|
|
219
|
+
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
220
|
+
scheme, colon, rest = value.partition(":")
|
|
221
|
+
authorities = [re.match(r"[^/?#]*", rest.lstrip("/\\")).group(0)] if colon else []
|
|
222
|
+
users = [authority.rpartition("@")[0] for authority in authorities]
|
|
223
|
+
return any(user.partition(":")[2] for user in users + [ascii_decoded(user) for user in users])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def repository_credential(value: str) -> bool:
|
|
227
|
+
"""FAA-198 (0.62.0 readings 6 and 7): whether a repository URI carries a credential: in its scheme and authority (a
|
|
228
|
+
user), in its query or fragment as secret_url reads them, or as a path segment shaped like a key. A path segment in
|
|
229
|
+
lower case, or of letters with no digit (task-pipeline-dashboard, Task-Pipeline-Dashboard), is the repository's
|
|
230
|
+
name, never a key, though it reads as an `sk-` shape, unless a key's shape starts it or follows a `.`, `_` or `-` in
|
|
231
|
+
it (repository_name); a segment is read as written and ascii_decoded. A
|
|
232
|
+
user outside a URL's authority (an scp form, `https:user@host/path`) counts as one: a user is an authority's."""
|
|
233
|
+
if "@" in value and not _REPOSITORY_AUTHORITY.match(value):
|
|
234
|
+
return True
|
|
235
|
+
match = _REPOSITORY_URI.match(value)
|
|
236
|
+
if not match:
|
|
237
|
+
return secret_argument(value)
|
|
238
|
+
head, path, tail = match.groups()
|
|
239
|
+
if repository_password(value):
|
|
240
|
+
return True
|
|
241
|
+
odd = [piece for written in (path, ascii_decoded(path)) for piece in re.split(r"[/\\:]", written)
|
|
242
|
+
if piece and not repository_name(piece)]
|
|
243
|
+
# the scheme and authority decoded too: git sends `https://%67hp_...@h` with its user decoded (unit reading 12)
|
|
244
|
+
return secret_url(head + tail) or secret_argument(ascii_decoded(head)) or any(secret_argument(piece) for piece in odd)
|
|
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
14
14
|
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.
|
|
15
15
|
metadata:
|
|
16
16
|
author: PassionCode.ai
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.62.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "3830a3353923e20f826d466610e1c479f120bec0"
|
|
@@ -65,16 +65,17 @@ Answer before writing code, and record the answers in the project README:
|
|
|
65
65
|
| What it stores | the store, where it lives (Step 2, rule 5), and what survives uninstall |
|
|
66
66
|
| Data and uptime | kind U or O, kit folders, a durable store or `<id>-data`, uptime A or B; a new agent's `LICENSE` holder, transfer audit per release (creating-fabric-agents) |
|
|
67
67
|
| What it tells the operator | the events worth a notification (Step 5) |
|
|
68
|
+
| Source | `source.repository`, required (Step 4); connections, `[]` if none |
|
|
68
69
|
|
|
69
70
|
## Step 1: choose the surfaces
|
|
70
71
|
|
|
71
72
|
| Caller | Surface | Why |
|
|
72
73
|
|---|---|---|
|
|
73
|
-
| Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session
|
|
74
|
+
| Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session (stdio-per-session once left twenty stray servers) |
|
|
74
75
|
| The operator or a script | a CLI: `<tool> service status\|start\|stop\|restart`, `<tool> dashboard` (opens only in Fabric Dashboards, below), `<tool> doctor --json` | every verb delegates to launchd and the well-known document, never to its own process table |
|
|
75
76
|
| A remote agent | A2A `1.0` over HTTPS, or MCP behind an authenticated gateway | loopback reachability is not authorization |
|
|
76
77
|
| An A2A peer on this computer | `surfaces.a2a` names a public Agent Card, served without the token; its interface URLs are loopback (DEC-0122); the kits read it, serving one is FAA-10 | the card carries no credential, process member or required Fabric extension |
|
|
77
|
-
| The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the
|
|
78
|
+
| The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the existing dashboards' CSRF defence |
|
|
78
79
|
|
|
79
80
|
Read [the surfaces and auth reference](references/surfaces-and-auth.md) when wiring
|
|
80
81
|
MCP registration into a client config, the login flow or the CSRF header.
|
|
@@ -213,8 +214,8 @@ descriptor's `fabricManifest` at a manifest whose service key names this
|
|
|
213
214
|
The installer, not the service, owns the plist and the descriptor. Sequence:
|
|
214
215
|
|
|
215
216
|
1. `write_descriptor(descriptor)`: refuses a port or `id.instance` another descriptor
|
|
216
|
-
claims
|
|
217
|
-
one more service the operator sees
|
|
217
|
+
claims, and a new one without `source.repository` (a rewrite warns). A preview or branch
|
|
218
|
+
copy is a second **instance**, never a second id: one more service the operator sees. Verify a release with its
|
|
218
219
|
own doctor or self-check before it replaces `default`; a temporary instance is uninstalled
|
|
219
220
|
(`launchd_uninstall`, `remove_descriptor`) as soon as its check is done, not left running.
|
|
220
221
|
2. `launchd_plist(...)` then `launchd_install(...)`: writes and lints the plist,
|
|
@@ -269,7 +270,7 @@ refusals.
|
|
|
269
270
|
**Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
|
|
270
271
|
agents, models, connectors, stores and channels, each with its state, from
|
|
271
272
|
`connections_extension()` / `connectionsExtension()` in `fabric_connections`. Never a credential in it, never a call
|
|
272
|
-
count (that is the usage report);
|
|
273
|
+
count (that is the usage report); details: [the wire reference](references/protocol.md).
|
|
273
274
|
|
|
274
275
|
The events feed is a **view over the log you already keep** (a jobs table, a JSONL
|
|
275
276
|
journal), not a second store. Each event is one sentence a person reads; set
|
|
@@ -298,7 +299,8 @@ rule it can reach and says which it could not:
|
|
|
298
299
|
python3 <skill-dir>/scripts/check_service.py <id>[.<instance>]
|
|
299
300
|
```
|
|
300
301
|
|
|
301
|
-
It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim,
|
|
302
|
+
It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim, `descriptor.source-repository`,
|
|
303
|
+
`well-known.connections`, well-known
|
|
302
304
|
shape, identity, latency, Host/Origin/cross-site guards, loopback bind, token file,
|
|
303
305
|
events auth and shape, single-use login, state outside code, instance lock, plist,
|
|
304
306
|
launchd pid equals answering pid, `dashboard.declared` (ED-9), `well-known.actions` (DEC-0121),
|
|
@@ -405,8 +407,8 @@ this estate needs.
|
|
|
405
407
|
- **Not macOS / no launchd:** the kits write a systemd user unit (Linux) or a per-user Scheduled Task (Windows); with
|
|
406
408
|
no supervisor at all declare `lifecycle.manager: "none"`, and hosts show the service read-only. Keep every other rule.
|
|
407
409
|
- **A desktop app with no supervisor** (DEC-0069): its descriptor says `manager: "person"` with `lifecycle.app`, no
|
|
408
|
-
origin, `installedBy {by: "person", role, tool}
|
|
409
|
-
|
|
410
|
+
origin, `installedBy {by: "person", role, tool}`, `source.repository` (the lifecycle reference, *An app's own
|
|
411
|
+
descriptor and the node*).
|
|
410
412
|
- **No Python:** use the Node kit; run the probe from any machine that has Python and
|
|
411
413
|
can reach the port, or mark the probe `NOT_RUN` with the reason.
|
|
412
414
|
- **Neither kit fits the framework:** implement the four routes and the lock by hand
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md
CHANGED
|
@@ -71,9 +71,16 @@ names the same file.
|
|
|
71
71
|
|
|
72
72
|
## Install into a service
|
|
73
73
|
|
|
74
|
+
The service that runs it says what it uses (FAA-199): `agent.connections()` names the two models, and the service adds
|
|
75
|
+
its own hand-off route, in its well-known document:
|
|
76
|
+
`fs.connections_extension(agent.connections() + [...], degraded=degraded)`. The two take the ids `model` and `fallback`
|
|
77
|
+
(a `degraded` entry whose source is `fallback` marks the fallback); the service's own items take other ids, since one id
|
|
78
|
+
named twice is refused.
|
|
79
|
+
|
|
74
80
|
```bash
|
|
75
81
|
python3 <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py \
|
|
76
|
-
install <service>/support --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths
|
|
82
|
+
install <service>/support --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths \
|
|
83
|
+
--module fabric_connections
|
|
77
84
|
cp <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py <service>/support/
|
|
78
85
|
python3 <adapter>/.../fabric_kit.py status <service>/support # current, older or modified, per file
|
|
79
86
|
```
|
|
@@ -24,8 +24,9 @@ For each message it:
|
|
|
24
24
|
What the agent does not do: send the reply anywhere (the service that calls it owns its channel), judge facts with a
|
|
25
25
|
model, or answer outside the knowledge file. The model's text is data: a citation it invents fails the check.
|
|
26
26
|
|
|
27
|
-
The kit modules
|
|
28
|
-
(`fabric_kit.py install <dir> --module fabric_openrouter
|
|
27
|
+
The kit modules sit beside this file in a service, installed as the README's "Install into a service" says
|
|
28
|
+
(`fabric_kit.py install <dir> --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths
|
|
29
|
+
--module fabric_connections`); in this repository they are read from ../../scripts.
|
|
29
30
|
Standard library only. FAA-65 REQ-09.
|
|
30
31
|
"""
|
|
31
32
|
from __future__ import annotations
|
|
@@ -371,6 +372,22 @@ class SupportAgent:
|
|
|
371
372
|
self._local = threading.local()
|
|
372
373
|
self._log = DecisionLog(self.state_dir)
|
|
373
374
|
|
|
375
|
+
def connections(self) -> List[Dict[str, Any]]:
|
|
376
|
+
"""FAA-199: what the agent uses, for `connections_extension` in the well-known document of the service that runs
|
|
377
|
+
it (DEC-0096); that service adds its own hand-off route and channels."""
|
|
378
|
+
# A model id that is not one (empty, missing, with a space) is left out, so a misconfigured fallback degrades the
|
|
379
|
+
# replies, never the well-known document (0.62.0 seam reading 1).
|
|
380
|
+
named = [("model", self.model, "drafts replies from the knowledge file"),
|
|
381
|
+
("fallback", self.fallback, "drafts when the first model fails")]
|
|
382
|
+
import fabric_connections # the service that declares them installs it (README); the agent runs without it
|
|
383
|
+
kept = []
|
|
384
|
+
for ident, target, purpose in named: # by the connections rule itself, never a second copy of it (seam reading 2)
|
|
385
|
+
item = {"id": ident, "kind": "model", "target": target, "purpose": purpose}
|
|
386
|
+
probe = {"extensions": {fabric_connections.CONNECTIONS_EXTENSION_KEY: {"connections": [dict(item, state="unknown")]}}}
|
|
387
|
+
if not fabric_connections.connections_rules(probe):
|
|
388
|
+
kept.append(item)
|
|
389
|
+
return kept
|
|
390
|
+
|
|
374
391
|
@property
|
|
375
392
|
def decisions_file(self) -> Path:
|
|
376
393
|
return self._log.path
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md
CHANGED
|
@@ -45,8 +45,10 @@ const answer = await request('GET', 'https://api.example/v1/items', { headers, d
|
|
|
45
45
|
|
|
46
46
|
Every answer comes back as a `Response`, whatever its status; `HttpError` means no usable answer came, with `kind`
|
|
47
47
|
`connection`, `timeout`, `too-large` or `refused`, the status when there was one, and the URL redacted to scheme,
|
|
48
|
-
host, port and path (`redact_url`); a URL that is not http or https keeps only its scheme (`htps:<redacted
|
|
49
|
-
|
|
48
|
+
host, port and path (`redact_url`); a URL that is not http or https keeps only its scheme (`htps:<redacted>`, and `ssh:<redacted>`
|
|
49
|
+
for `ssh://h:99999/r` too), and an http, https, ws, wss or ftp URL that does not parse (Node reads it with `new URL`;
|
|
50
|
+
Python reads its own special-scheme form and prints what it finds, never the user information), or a value with no
|
|
51
|
+
scheme, is `<unparseable url>`. The path is not redacted (Node writes it as `new URL`
|
|
50
52
|
normalises it, Python as given), so a key never belongs in it. Python's `check_url` refuses a host
|
|
51
53
|
with a backslash, space, control character (C0, DEL or C1) or `%`, a URL with a space or ASCII control character before its
|
|
52
54
|
first `#` (the default transport sends a URL up to there, as Node's does; a transport you pass receives the URL as given), or a character outside ASCII in its path or query (ASCII whitespace at its ends is trimmed, as urllib and `new URL` trim it; a Unicode space ending the part before the first `#` is refused, where Node keeps it percent-encoded), a bracketed host that is not an IPv6 address with no zone, and a port that is not
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md
CHANGED
|
@@ -133,8 +133,8 @@ part before the first `#` is refused, as `check_url` refuses it, because Node ke
|
|
|
133
133
|
trim it and read the keys when it ends the URL); a leading space is refused as not a URL), a host holding `%`, a backslash, a Unicode space or a control character (urllib
|
|
134
134
|
would decode or dial it as another host), a bracketed host that is no IPv6 address, a port that is not ASCII digits or is above
|
|
135
135
|
65535, no host, and an empty userinfo. The Node kit accepts several of these as `new URL` reads them, among them a `%`
|
|
136
|
-
host decoded, a backslash as `/`, a tab or line break removed
|
|
137
|
-
|
|
136
|
+
host decoded, a backslash as `/`, a tab or line break removed and an extra slash before the host skipped. A user
|
|
137
|
+
information as written is refused by both kits, empty or holding a backslash (since 0.62.0). The
|
|
138
138
|
Python kit verifies Ed25519 itself (RFC 8032, standard library only, strict as OpenSSL: `S` below the group order,
|
|
139
139
|
canonical points, no small-order key); it follows no redirect when it reads the key set; the Node kit uses `node:crypto` and refuses small-order keys
|
|
140
140
|
itself (`isSmallOrderKey`), because Node before 26 lets OpenSSL accept a signature under one.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md
CHANGED
|
@@ -202,6 +202,8 @@ const { dirs, instance, lock } = await holdContextInstance('example-agent', 'org
|
|
|
202
202
|
`~/Library/Application Support/<id>.<instance>`, `~/Library/Logs/<id>.<instance>`, `~/Library/Caches/<id>.<instance>`),
|
|
203
203
|
never inside the personal tree. `launchd_uninstall(..., purge=True, service_id=..., instance=...)` /
|
|
204
204
|
`launchdUninstall(..., {purge, serviceId, instance})` purge only that instance's folders.
|
|
205
|
+
- **Its descriptor names its repository.** A new instance is a new install, so its descriptor passes
|
|
206
|
+
`source.repository` as the personal one does (FAA-198, since 0.62.0); without it the write is refused.
|
|
205
207
|
- **What it checks, before anything is created.** `context_instance` / `contextInstance` is pure and answers
|
|
206
208
|
`{context, kind, org?, instance, key, dirs, descriptorPath, grant?}`, never the record. An organization's instance
|
|
207
209
|
needs the estate record revision this machine applied (verified with `verify_estate_record`; the helper verifies no
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -86,7 +86,8 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
|
|
|
86
86
|
|
|
87
87
|
1. Build or unpack the release into `~/.local/share/<id>/releases/<version>-<sha12>/`
|
|
88
88
|
(a venv or `node_modules` inside it). Never edit a release after it is written.
|
|
89
|
-
2. `write_descriptor` (port and id claims are checked here
|
|
89
|
+
2. `write_descriptor` (port and id claims are checked here, and a new descriptor without `source.repository` is
|
|
90
|
+
refused; a rewrite of an installed one warns, FAA-198).
|
|
90
91
|
3. `launchd_install` (Node: `await launchdInstall(...)`, the same steps): write + `plutil -lint`, read `launchctl print-disabled gui/<uid>`,
|
|
91
92
|
`enable` **only when no override is recorded** (a first install), `bootout` and wait
|
|
92
93
|
until `launchctl print gui/<uid>/<label>` fails, `bootstrap` with up to five retries,
|
|
@@ -150,7 +151,8 @@ never infers supervision from its parent process.
|
|
|
150
151
|
|
|
151
152
|
**An app** a person installs and starts has no supervisor (contract DEC-0069). Its descriptor says
|
|
152
153
|
`"lifecycle": {"manager": "person", "app": "<the installed bundle or executable>"}`, `installedBy {by: "person", role,
|
|
153
|
-
tool}
|
|
154
|
+
tool}`, `source.repository` (the app's own repository; its first write is refused without it, FAA-198) and no `origin`:
|
|
155
|
+
it says the app is installed, never that it runs. The installer writes it; where there is none
|
|
154
156
|
(a macOS app dragged into Applications), the app writes it on a start when `app_descriptor_action` /
|
|
155
157
|
`appDescriptorAction` answers `write` (no descriptor yet, another version in `installedBy.tool`, or another path than
|
|
156
158
|
`lifecycle.app`), and never from an App Translocation path. Its live origin and process id go in its own liveness file,
|
|
@@ -15,10 +15,12 @@ Order matters: each step leaves the service answering.
|
|
|
15
15
|
delete it with the operator's consent.
|
|
16
16
|
6. **Regenerate the plist** from `launchd_plist`, pointing at a release; delete stray
|
|
17
17
|
`.plist.bak` files from `~/Library/LaunchAgents` after moving them elsewhere.
|
|
18
|
-
7. **Write the descriptor** from the installer
|
|
19
|
-
moving the newcomer, never the established service.
|
|
20
|
-
8. **
|
|
21
|
-
|
|
18
|
+
7. **Write the descriptor** from the installer, with `source.repository` (a new descriptor without it is refused);
|
|
19
|
+
resolve any port claim it refuses by moving the newcomer, never the established service.
|
|
20
|
+
8. **Declare what it uses** with `connections_extension` / `connectionsExtension` in the well-known document; a
|
|
21
|
+
service that uses nothing outside itself declares an empty list (`connections_extension([])`), which reads `PASS`.
|
|
22
|
+
9. **Re-run the probe** until nothing `FAIL`s and `descriptor.source-repository` and `well-known.connections` read
|
|
23
|
+
`PASS` (a `NOT_RUN` is not a pass); record the table in the service's verification document.
|
|
22
24
|
|
|
23
25
|
## Gaps that existing services usually have
|
|
24
26
|
|
|
@@ -55,7 +55,8 @@ File `<id>.<instance>.json`, mode 0600 (on Windows a protected ACL), atomic.
|
|
|
55
55
|
| `lifecycle` | `manager` `launchd` (then `label` and `plist`), `systemd` (then `unit`, a `systemctl --user` unit), `task-scheduler` (then `task`, a per-user Scheduled Task), `node` (Project Observatory's node supervises it as its child, with no supervisor file, DEC-0061), `person` (an app a person installs and starts, then `app`, the installed app, DEC-0069) or `none`; `unit` and `task` are refused beside any other manager (DEC-0032), `label` and `plist` beside any but `launchd` (DEC-0057), `app` beside any but `person`. Both kits and the probe accept all six; both kits write the unit and the task (`systemd_unit`, `task_xml` / `systemdUnit`, `taskXml`). An app's descriptor says it is installed, never that it runs: the probe checks `app` is still there (`descriptor.app-installed`) and reads no well-known document; a reader that finds the app gone treats the descriptor as stale (`app_descriptor_stale` / `appDescriptorStale`), and an app that writes its own descriptor rewrites it only when `app_descriptor_action` / `appDescriptorAction` says `write` |
|
|
56
56
|
| `paths` | `data` (required), `logs[]`, optional `config`, `cache` |
|
|
57
57
|
| `commands` | only `doctor` and `update`, argument arrays whose first item is an absolute or `~/` path (Windows: also `C:\…`, `C:/…` or `~\…`, never a network share; DEC-0032) |
|
|
58
|
-
| `source.repository
|
|
58
|
+
| `source.repository` | the repository of the service's code (`https://github.com/owner/name`), which links the agent to its project. Optional in the schema; the kit's `write_descriptor` / `writeDescriptor` refuse a new descriptor without it since 0.62.0 (`source_refusal` / `sourceRefusal` says why, FAA-198), and rewriting one already installed without it warns (`NoSourceRepositoryWarning`; Node `process.emitWarning` with code `FABRIC_NO_SOURCE_REPOSITORY`), so an installed agent's update keeps working. The probe names its absence (`descriptor.source-repository`, `NOT_RUN`) |
|
|
59
|
+
| `fabricManifest` | optional |
|
|
59
60
|
| `installedAt`, `installedBy` | when, and who installed it: the installer and its version as a string, or `{by, role?, tool}` (DEC-0061): `by` `person` (with `role` `operator` or `administrator`), `node` or `organization`; `role` for a person only; `tool` the installer and its version. It names a role, never a person. An app's descriptor uses the object form with `by: "person"`. `register_remote` / `registerRemote` and the sample write the object form |
|
|
60
61
|
| `runner` | optional `{kind?, credentialKind}` (DEC-0061): `credentialKind` `subscription`, `key`, `platform-metered` or `none`: a kind, never a credential (`FAC-SEM-086` refuses a credential-shaped value here or in `installedBy`); a subscription names its vendor's runner (`claude-code`, `kimi-code`, `codex`) |
|
|
61
62
|
| `peer` | optional `{endpoint, custody, residency, maxDataClass}` (DEC-0061): the service is offered to others. `endpoint` `https://<dns-name>[:<port>][/<path>]`, never a reserved name (`FAC-SEM-085`); `custody` `organization-managed`, `personal` or `processor-hosted` (an organization's cloud a processor runs under a processing agreement, DEC-0099); `residency` 1–32 ISO 3166-1 alpha-2 codes, `EU` admitted, user-assigned codes refused; `maxDataClass` `public` < `internal` < `confidential` < `restricted`. An offer needs `runner` and never runs on a subscription. A host sends a request from anyone but the operator only when `peer_refusal` / `peerRefusal` answers null (`FAC-SEM-088`); usage rows may take `route: "platform-metered"` |
|
|
@@ -127,7 +128,8 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
127
128
|
`connectionsExtension(declared, {degraded, states})`: `state` comes from the service's own checks (`states`, by
|
|
128
129
|
`id`), else `degraded` when `degraded[]` names it, else `unknown`; a declaration a host would hide is refused with
|
|
129
130
|
`ServiceError`, and `build_well_known` refuses one passed in by hand. `connections_view` / `connectionsView` read
|
|
130
|
-
a peer's list as Fabric Dashboards does, and `check_service.py` reports `well-known.connections
|
|
131
|
+
a peer's list as Fabric Dashboards does, and `check_service.py` reports `well-known.connections` (`NOT_RUN` when
|
|
132
|
+
the document has no connections declaration, FAA-199, and `PASS` for an empty list; the reference service declares its template and its demo source). Build it each
|
|
131
133
|
time the well-known document is built, from the same `degraded` list:
|
|
132
134
|
|
|
133
135
|
```python
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -42,8 +42,18 @@ same objects (well-known document, events feed, login code) at the same paths.
|
|
|
42
42
|
- `commands` may carry `doctor` (a local executable); never `update`.
|
|
43
43
|
- `source.repository` names the repository of the service's code, so Fabric Dashboards can tell code apart from a store
|
|
44
44
|
that versions data. Pass it as `registerRemote({ ..., source: { repository } })` / `register_remote(..., source=...)`
|
|
45
|
-
(since 0.49.3
|
|
46
|
-
|
|
45
|
+
(since 0.49.3; a new registration without it is refused since 0.62.0, before the token file is written, and a
|
|
46
|
+
rotation that passes none keeps the installed one, FAA-198); the
|
|
47
|
+
kit checks it as the schema's URI (RFC 3986 characters, a host and a valid port when it names an
|
|
48
|
+
authority) and refuses a `user:password@`, a user outside a URL's authority (an scp form), and a value shaped like a
|
|
49
|
+
credential in its user, query or fragment or as a path segment; a path segment in lower case, or of letters with no
|
|
50
|
+
digit, is the repository's name unless a key's shape starts it or follows a `.`, `_` or `-` in it (since 0.62.0, so
|
|
51
|
+
`task-pipeline-dashboard` and `Task-Pipeline-Dashboard` are accepted, a name that starts with `sk-` and 16 more
|
|
52
|
+
characters is not, in any case, nor `%73k-` and the rest; a host is read for a key's shape anywhere in it, inside a word too, so
|
|
53
|
+
`task-pipeline-dashboard.example.com` is refused, FAA-200 (32)). A path and a user information are read as written and with
|
|
54
|
+
their ASCII escapes decoded, and a password as written and as the URL standard reads it (`https://h\u:pw@x/r` is
|
|
55
|
+
refused). A checkout's origin in scp form (`git@host:owner/name.git`) is refused: write it `https://host/owner/name`,
|
|
56
|
+
the repository's own page with no path below it, no query and no fragment, which the estate's readers join.
|
|
47
57
|
- `installedBy` is the installer's name and version as a string (`"example-installer 1.2.0"`), or
|
|
48
58
|
`{by, role?, tool}` where `by` is `person`, `node` or `organization` and `role` (`operator`, `administrator`) is a
|
|
49
59
|
person's only. There is no `agent` kind: an agent that registers a service writes the string form, or
|
|
@@ -111,5 +121,8 @@ The login rules keep every `Set-Cookie` of the answer and judge the session cook
|
|
|
111
121
|
|
|
112
122
|
Rotate the token by setting the new value on the platform and re-running `registerRemote` on the
|
|
113
123
|
operator's computer with it; the old token stops working the moment the platform restarts with the
|
|
114
|
-
new value, and so does every session begun under it when `LoginCodes` has `token=`.
|
|
124
|
+
new value, and so does every session begun under it when `LoginCodes` has `token=`. The re-run keeps the
|
|
125
|
+
installed `source` when it passes none (one the kit refuses, edited by hand, is dropped with the warning); pass
|
|
126
|
+
`runner`, `peer`, `summary`, `doctor` and `installedBy` / `installed_by` again, since a re-run writes only what it is
|
|
127
|
+
given (without `installedBy` it writes the default person form). Rotate the
|
|
115
128
|
session key on the platform to sign every operator out.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py
CHANGED
|
@@ -64,6 +64,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
64
64
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
65
65
|
import fabric_service as fs # noqa: E402
|
|
66
66
|
import fabric_connections as fcn # noqa: E402
|
|
67
|
+
from check_service_rules import shown_repository # noqa: E402 - a repository in evidence carries no userinfo or query
|
|
67
68
|
import fabric_interop as fi # noqa: E402
|
|
68
69
|
|
|
69
70
|
Result = Dict[str, str]
|
|
@@ -194,6 +195,18 @@ class Probe:
|
|
|
194
195
|
else:
|
|
195
196
|
unique = "port %d is unique" % self.port
|
|
196
197
|
self.add("descriptor.port-claim", "FAIL" if clashes else "PASS", "; ".join(clashes) or unique)
|
|
198
|
+
# FAA-198: the repository that links the agent to its project; a new install is refused without it, an installed one
|
|
199
|
+
# is named here (NOT_RUN is no pass) until its owner adds it.
|
|
200
|
+
# A source descriptor.valid refuses is no pass either, and the evidence never carries a URL's userinfo or query,
|
|
201
|
+
# where a password rides (0.62.0 unit reading 2).
|
|
202
|
+
if not fs.names_repository(self.d):
|
|
203
|
+
self.add("descriptor.source-repository", "NOT_RUN", "the descriptor %s" % fs.NO_SOURCE_REPOSITORY)
|
|
204
|
+
# what the source adds, read in the member rules, which a missing required key never cuts short (unit reading 16)
|
|
205
|
+
elif added := sorted(set(fs.descriptor_member_problems(self.d)) - set(fs.descriptor_member_problems(
|
|
206
|
+
{k: v for k, v in self.d.items() if k != "source"}))): # its own problem (a member name it names is the descriptor's own, FAA-200 (39))
|
|
207
|
+
self.add("descriptor.source-repository", "NOT_RUN", "source is refused: %s" % added[0])
|
|
208
|
+
else:
|
|
209
|
+
self.add("descriptor.source-repository", "PASS", "names %s" % shown_repository(self.d["source"]["repository"]))
|
|
197
210
|
if (self.d.get("lifecycle") or {}).get("manager") == "person":
|
|
198
211
|
# DEC-0069: installed, never running — a reader checks the app is still there; liveness is the app's own file.
|
|
199
212
|
present = fs.app_present(self.d)
|
|
@@ -275,6 +288,9 @@ class Probe:
|
|
|
275
288
|
self.add("well-known.connections", "FAIL" if view["reported"] else "PASS",
|
|
276
289
|
"; ".join(f["message"] for f in fcn.connections_rules(wk)[:5]) or
|
|
277
290
|
"%d connection(s), %d named in degraded[]" % (len(view["connections"]), failing))
|
|
291
|
+
else: # FAA-199: named, never a pass, so the connection map is not left empty unseen
|
|
292
|
+
self.add("well-known.connections", "NOT_RUN", "declares no %s: a host cannot draw what the service uses "
|
|
293
|
+
"(connections_extension / connectionsExtension)" % fcn.CONNECTIONS_EXTENSION_KEY)
|
|
278
294
|
# DEC-0121, FAC-SEM-142: every button's agent twin, declared; no declaration is NOT_RUN (D10).
|
|
279
295
|
self.add("well-known.actions", *well_known_actions_verdict(wk))
|
|
280
296
|
# DEC-0122: where the public Agent Card is; no member is NOT_RUN.
|
|
@@ -809,9 +825,9 @@ class Probe:
|
|
|
809
825
|
if not source:
|
|
810
826
|
self.add(rule, "NOT_RUN", "data is inside repository %s and the descriptor names no source.repository to tell code from data" % top)
|
|
811
827
|
elif remote.returncode == 0 and same_repository(remote.stdout.strip(), source):
|
|
812
|
-
self.add(rule, "FAIL", "data lives in the service's own code checkout %s (%s)" % (top, source))
|
|
828
|
+
self.add(rule, "FAIL", "data lives in the service's own code checkout %s (%s)" % (top, shown_repository(source)))
|
|
813
829
|
else:
|
|
814
|
-
self.add(rule, "PASS", "data is in repository %s, which is not the service's code (%s)" % (top, remote.stdout.strip() or "no remote"))
|
|
830
|
+
self.add(rule, "PASS", "data is in repository %s, which is not the service's code (%s)" % (top, shown_repository(remote.stdout.strip()) or "no remote"))
|
|
815
831
|
|
|
816
832
|
def lock_rule(self, data: Path) -> None:
|
|
817
833
|
lock = data / "service.lock"
|