@passioncode-ai/passioncode 0.2.45 → 0.2.46
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 +9 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +134 -24
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.46 - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Fabric Agent Adapter is pinned at `v0.56.0`** (it was `v0.55.0`): stability and Windows. working-with-fabric names
|
|
8
|
+
the repositories a Windows turn touched; the board responder recovers from a refused slot, leaves within 3 s when
|
|
9
|
+
idle and logs its notices at `warning` with a subject; the reference service can take a moved slot back; a spend
|
|
10
|
+
gate fails closed.
|
|
11
|
+
|
|
3
12
|
## 0.2.45 - 2026-10-27
|
|
4
13
|
|
|
5
14
|
### Changed
|
package/family.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.46",
|
|
4
4
|
"description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "fabric-agent-adapter",
|
|
12
12
|
"displayName": "Fabric Agent Adapter",
|
|
13
13
|
"source": "./plugins/fabric-agent-adapter",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.56.0",
|
|
15
15
|
"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.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "PassionCode.ai",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.2.
|
|
38
|
+
"version": "0.2.46",
|
|
39
39
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
package/payload/manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.46",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
7
7
|
"name": "fabric-agent-adapter",
|
|
8
8
|
"displayName": "Fabric Agent Adapter",
|
|
9
9
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
10
|
-
"ref": "v0.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.
|
|
10
|
+
"ref": "v0.56.0",
|
|
11
|
+
"commit": "250d5d7a18980bacdfc7ba3bb449794995d4c07d",
|
|
12
|
+
"version": "0.56.0",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"legacyMarketplaces": [
|
|
24
24
|
"fabric-agent-adapter"
|
|
25
25
|
],
|
|
26
|
-
"contentHash": "sha256:
|
|
26
|
+
"contentHash": "sha256:a27c7b99d9b29039b28aea11680bf1acce43a6c7792b8e80b16310559d424856",
|
|
27
27
|
"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.",
|
|
28
28
|
"author": {
|
|
29
29
|
"name": "PassionCode.ai",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"name": "passioncode",
|
|
63
63
|
"displayName": "PassionCode.ai",
|
|
64
64
|
"repo": "passioncode-ai/passioncode",
|
|
65
|
-
"ref": "v0.2.
|
|
65
|
+
"ref": "v0.2.46",
|
|
66
66
|
"commit": null,
|
|
67
|
-
"version": "0.2.
|
|
67
|
+
"version": "0.2.46",
|
|
68
68
|
"via": "this repository",
|
|
69
69
|
"skills": [
|
|
70
70
|
"setting-up-passioncode",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
],
|
|
73
73
|
"legacyPluginIds": [],
|
|
74
74
|
"legacyMarketplaces": [],
|
|
75
|
-
"contentHash": "sha256:
|
|
75
|
+
"contentHash": "sha256:26a83f55d769e493d16987ae596bb26288fa03f2b2f81231d22d5298ccd603bc",
|
|
76
76
|
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
77
77
|
"author": {
|
|
78
78
|
"name": "PassionCode.ai",
|
|
@@ -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.56.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (
|
|
2
|
+
"description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (unless FABRIC_SESSION_NO_REPOSITORIES=1), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0 once python3 starts (on Windows it must name Python in Git Bash, FAA-183), and hands any Fabric call to a detached worker with a 10 s deadline.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -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.56.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
|
|
11
11
|
---
|
|
@@ -47,7 +47,11 @@ ENV_MAX = 64
|
|
|
47
47
|
# Whole-value patterns end in \Z, never `$` (R1: `$` also matches before a trailing newline).
|
|
48
48
|
_ID = re.compile(r"^[a-z][a-z0-9-]{1,62}\Z")
|
|
49
49
|
_ENV_NAME = re.compile(r"^[A-Z_][A-Z0-9_]*\Z")
|
|
50
|
-
|
|
50
|
+
# A special scheme (http, https, ws, wss, ftp) reads a user:password@ after any run of slashes, none included, as
|
|
51
|
+
# `new URL` does: https:bot:pw@host carries the password too (0.56.0 seam r7).
|
|
52
|
+
_URI_PASSWORD = re.compile(r"^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt][Pp]):[/\\]*[^/\\?#]*:[^/\\?#]*@"
|
|
53
|
+
# any other scheme: a backslash belongs to the user information (seam r8)
|
|
54
|
+
r"|(?![Hh][Tt][Tt][Pp][Ss]?:|[Ww][Ss][Ss]?:|[Ff][Tt][Pp]:)[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*:[^/?#]*@)")
|
|
51
55
|
_SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})\Z")
|
|
52
56
|
_LOCAL_PATH = re.compile(LOCAL_PATH_PATTERN)
|
|
53
57
|
_URL = re.compile(URL_PATTERN)
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
"""The contract's one credential detector (Fabric Agent Contract DEC-0044, `src/secret-shape.ts` at the pin): a name
|
|
3
3
|
that can only carry a credential, a value shaped like one, a launch argument that carries one, and a URL that does
|
|
4
4
|
(LC-21). A port held to the contract's own answers by test/fixtures/contract/profile-vectors.json. Standard library
|
|
5
|
-
only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
|
|
5
|
+
only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
|
|
6
|
+
|
|
7
|
+
It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
|
|
8
|
+
U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
9
12
|
import re
|
|
10
|
-
from typing import Dict, List
|
|
13
|
+
from typing import Dict, List, Tuple
|
|
11
14
|
import urllib.parse
|
|
12
15
|
|
|
13
16
|
CREDENTIAL_WORDS = (
|
|
@@ -22,20 +25,28 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
|
|
|
22
25
|
"session": ("token", "id"),
|
|
23
26
|
"code": ("login", "auth", "access", "session"),
|
|
24
27
|
}
|
|
28
|
+
#: ECMAScript's \s, as a character class body.
|
|
29
|
+
JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
30
|
+
_NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
|
|
25
31
|
JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
|
|
26
|
-
SECRET_VALUE = re.compile(r"(^bearer
|
|
27
|
-
re.IGNORECASE | re.
|
|
32
|
+
SECRET_VALUE = re.compile(r"(^bearer[%s]%s+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}"
|
|
33
|
+
r"|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})" % (JS_SPACE, _NOT_LINE_END), re.IGNORECASE | re.ASCII)
|
|
28
34
|
_LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
|
|
29
|
-
_BEARER_INSIDE = re.compile(r"
|
|
30
|
-
|
|
35
|
+
_BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
|
|
36
|
+
re.IGNORECASE | re.ASCII)
|
|
37
|
+
_PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
|
|
38
|
+
_WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
|
|
39
|
+
_ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
|
|
31
40
|
_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
|
|
41
|
+
#: WHATWG's special schemes that carry an authority with user information (file has none).
|
|
42
|
+
_SPECIAL = ("ftp", "http", "https", "ws", "wss")
|
|
32
43
|
|
|
33
44
|
|
|
34
45
|
def name_words(name: str) -> List[str]:
|
|
35
46
|
"""A name's words: split at `.`, `_`, `-` and camelCase humps, lowercased (`APIKey` is `api key`)."""
|
|
36
47
|
spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name)
|
|
37
48
|
spaced = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", spaced)
|
|
38
|
-
return [w.lower() for w in
|
|
49
|
+
return [w.lower() for w in _WORD_SPLIT.split(spaced) if w]
|
|
39
50
|
|
|
40
51
|
|
|
41
52
|
def _with_singulars(words: List[str]) -> List[str]:
|
|
@@ -73,7 +84,7 @@ def secret_argument(value: str) -> bool:
|
|
|
73
84
|
so a folder path holding a session id is not taken for a key."""
|
|
74
85
|
if JWT_SHAPE.search(value) or SECRET_VALUE.search(value) or _BEARER_INSIDE.search(value):
|
|
75
86
|
return True
|
|
76
|
-
for piece in
|
|
87
|
+
for piece in _ARGUMENT_SPLIT.split(value):
|
|
77
88
|
if _path_like(piece):
|
|
78
89
|
if any(_long_secret(part) for part in re.split(r"[\\/]", piece)):
|
|
79
90
|
return True
|
|
@@ -93,7 +104,8 @@ def _decode_keeping_plus(raw: str) -> str:
|
|
|
93
104
|
|
|
94
105
|
|
|
95
106
|
def _form_name(text: str) -> str:
|
|
96
|
-
|
|
107
|
+
# URLSearchParams drops one leading `?` from the text it is given (0.56.0 unit r7).
|
|
108
|
+
pairs = urllib.parse.parse_qsl(text[1:] if text.startswith("?") else text, keep_blank_values=True)
|
|
97
109
|
return pairs[0][0] if pairs else ""
|
|
98
110
|
|
|
99
111
|
|
|
@@ -109,23 +121,66 @@ def raw_values(query: str) -> Dict[str, List[str]]:
|
|
|
109
121
|
return values
|
|
110
122
|
|
|
111
123
|
|
|
124
|
+
#: The C0 controls and space, U+0000 to U+0020: what WHATWG URL parsing trims from a URL's ends.
|
|
125
|
+
_C0_OR_SPACE = "".join(map(chr, range(0x21)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
_LONE_SURROGATE = re.compile("[\ud800-\udfff]")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _percent_encode(text: str, also: str) -> str:
|
|
132
|
+
"""WHATWG's percent-encoding of a query or a fragment: every C0 control, space, `"`, `<`, `>`, every code point
|
|
133
|
+
above U+007E and the characters in `also`, as UTF-8 %XX (a lone surrogate as U+FFFD, as a USVString reads it).
|
|
134
|
+
`new URL` hands the contract's secretUrl the text in this form, so a malformed escape left in it is read the same
|
|
135
|
+
way by both ports (0.56.0 unit r8)."""
|
|
136
|
+
out = []
|
|
137
|
+
for char in _LONE_SURROGATE.sub("\ufffd", text):
|
|
138
|
+
if ord(char) <= 0x20 or ord(char) > 0x7E or char in '"<>' or char in also:
|
|
139
|
+
out.append("".join("%%%02X" % byte for byte in char.encode("utf-8")))
|
|
140
|
+
else:
|
|
141
|
+
out.append(char)
|
|
142
|
+
return "".join(out)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _url_parts(value: str) -> Tuple[str, str, str]:
|
|
146
|
+
"""The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
|
|
147
|
+
refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
|
|
148
|
+
r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
|
|
149
|
+
after every slash or backslash that follows `scheme:` (the special authority ignore slashes state) and ends at the
|
|
150
|
+
next slash, backslash, `?` or `#`, so `https:u:p@h` and `https:\\u:p@h` carry the password p; any other scheme
|
|
151
|
+
has an authority only after `//`, and `file` has no user information. The user information ends at the authority's
|
|
152
|
+
last `@`, and the password follows its first `:`. A host `new URL` would refuse is not checked: such a URL is read
|
|
153
|
+
for its password and parameters all the same, the safe direction for a detector."""
|
|
154
|
+
scheme, _, rest = value.partition(":")
|
|
155
|
+
rest, _, fragment = rest.partition("#")
|
|
156
|
+
rest, _, query = rest.partition("?")
|
|
157
|
+
lowered = scheme.lower()
|
|
158
|
+
if lowered in _SPECIAL:
|
|
159
|
+
authority = rest.replace("\\", "/").lstrip("/").partition("/")[0]
|
|
160
|
+
elif lowered != "file" and rest.startswith("//"):
|
|
161
|
+
authority = rest[2:].partition("/")[0]
|
|
162
|
+
else:
|
|
163
|
+
authority = ""
|
|
164
|
+
userinfo, at, _ = authority.rpartition("@")
|
|
165
|
+
return ((userinfo.partition(":")[2] if at else ""), _percent_encode(query, "'" if lowered in _SPECIAL or lowered == "file" else ""), # file is special too
|
|
166
|
+
_percent_encode(fragment, "`"))
|
|
167
|
+
|
|
168
|
+
|
|
112
169
|
def secret_url(value: str) -> bool:
|
|
113
170
|
"""Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
|
|
114
171
|
password in its user information, or a query or fragment parameter whose name can only carry a credential and has
|
|
115
172
|
a value, or whose value is shaped like one. A string that is not an absolute URL is checked as an argument only."""
|
|
116
173
|
if secret_argument(value):
|
|
117
174
|
return True
|
|
118
|
-
|
|
175
|
+
# What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
|
|
176
|
+
# refuses), then remove every tab, CR and LF.
|
|
177
|
+
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
178
|
+
if not _SCHEME.match(value):
|
|
119
179
|
return False # not an absolute URL (WHATWG URL parsing refuses it)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
except ValueError:
|
|
123
|
-
return False
|
|
124
|
-
if not parts.scheme:
|
|
125
|
-
return False
|
|
126
|
-
if parts.password:
|
|
180
|
+
password, query, fragment = _url_parts(value)
|
|
181
|
+
if password:
|
|
127
182
|
return True
|
|
128
|
-
for part in (
|
|
183
|
+
for part in (query, fragment):
|
|
129
184
|
for name, raws in raw_values(part).items():
|
|
130
185
|
if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
|
|
131
186
|
return True
|
|
@@ -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.56.0"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
|
|
@@ -104,8 +104,8 @@ responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) f
|
|
|
104
104
|
every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
|
|
105
105
|
The request body is data behind the service's own input gate. From 0.53.0 a slot the operator moved to another
|
|
106
106
|
session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate `relayed`, or it
|
|
107
|
-
stays moved for good (
|
|
108
|
-
or its elicitation). Read [the board reference](references/board.md).
|
|
107
|
+
stays moved for good (the operator reaches it through an agent that relays their decision in `_meta`, the service's
|
|
108
|
+
page or its elicitation; Claude Code and Codex wait for CT-125's forwarding). Read [the board reference](references/board.md).
|
|
109
109
|
|
|
110
110
|
## Step 2: the non-negotiables
|
|
111
111
|
|
|
@@ -260,8 +260,9 @@ or `human` with its `humanStep`) and declared with `build_well_known(actions=ser
|
|
|
260
260
|
`extensions/actions/0.1`. A `relayed` action called by the operator's own agent with their decision in
|
|
261
261
|
`_meta.decision` is the operator's act: never answer it "operator session required" or send them to the page.
|
|
262
262
|
A service adopts the relayed gate once Fabric's hub forwards decisions (CT-125), as the contract says; until then its tools
|
|
263
|
-
answer as today, except the board responder's take-back action, declared `relayed` now
|
|
264
|
-
|
|
263
|
+
answer as today, except the board responder's take-back action, declared `relayed` now: an agent whose client sets
|
|
264
|
+
`_meta` calls it with the operator's decision; Claude Code and Codex reach it through the page until CT-125
|
|
265
|
+
([board.md](references/board.md); the reference service's `board.take-back`). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
|
|
265
266
|
refusals.
|
|
266
267
|
|
|
267
268
|
**Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md
CHANGED
|
@@ -42,7 +42,9 @@ operator gives in Fabric (Agents & access): callee `fabric`, capability `board.r
|
|
|
42
42
|
|
|
43
43
|
The responder runs inside the service process, started after the instance lock like every other piece of state
|
|
44
44
|
(SKILL.md, Step 3). [`sample_service.py`](../scripts/sample_service.py) does it behind `serve --board-project <uuid>`,
|
|
45
|
-
off by default
|
|
45
|
+
off by default (register it with the same `--board-project`, so its manifest lists the take-back tool below; a serve
|
|
46
|
+
whose board mode differs from the registered manifest at its start logs `service.manifest.mismatch` and leaves the
|
|
47
|
+
manifest as it is):
|
|
46
48
|
|
|
47
49
|
```python
|
|
48
50
|
import fabric_board as fbd, fabric_hub as fhb
|
|
@@ -53,12 +55,13 @@ responder = fbd.Responder(
|
|
|
53
55
|
handler=render, admit=gate, observe=look_on_disk, emit=log.append, service="example-agent")
|
|
54
56
|
board = threading.Thread(target=responder.run, args=(stop_event, 5.0), daemon=True) # SIGTERM: set stop_event
|
|
55
57
|
board.start()
|
|
56
|
-
# ... at stop, after stop_event is set: wait for it to leave its board session (a clean leave forgets
|
|
57
|
-
# records, FAA-155), bounded inside the kit's hard exit, as scripts/sample_service.py does
|
|
58
|
+
# ... at stop, after stop_event is set: wait for it to leave its board session (a clean leave forgets the slot
|
|
59
|
+
# records it confirmed, FAA-155), bounded inside the kit's hard exit, as scripts/sample_service.py does
|
|
58
60
|
board.join(timeout)
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
Node: `new Responder({...same names in camelCase})`, then `responder.run({ signal, drain })
|
|
63
|
+
Node: `new Responder({...same names in camelCase})`, then `const running = responder.run({ signal, drain })`; at stop,
|
|
64
|
+
abort the signal and `await running`, bounded inside the hard exit (the run leaves its board session before it settles). A service that lists
|
|
62
65
|
`fabric-project-comms/0.1` in its well-known `protocols` (DEC-0067: only once its hosts are pinned at or after it)
|
|
63
66
|
declares that it answers board requests; the probe checks that declaration.
|
|
64
67
|
|
|
@@ -144,6 +147,28 @@ answers `not_available` or `capacity_exceeded`, which do not say whether the rep
|
|
|
144
147
|
in line and the next tick asks again, and a restart asks with the same old fence. A request leaves the resume queue only
|
|
145
148
|
once it has resumed: when the `com.complete` that sends a known outcome loses its answer, the next tick sends the same
|
|
146
149
|
key again in the same session (FAA-86). `shutdown()` drains, then leaves the session (its DELETE releases the slot).
|
|
150
|
+
An idle responder waits 3 s at most for that DELETE (`IDLE_LEAVE_SECONDS`), so an idle stop ends within the org's 5 s
|
|
151
|
+
(LC-01, FAA-173); one with work waits up to the session's 15 s client timeout. A board call already in flight when the
|
|
152
|
+
stop comes (a poll against a slow hub) runs first; when the service's bounded join ends before the DELETE is sent, the
|
|
153
|
+
slot lapses on the board's clock as before. The slot records are forgotten only after
|
|
154
|
+
a DELETE Fabric answered with a 2xx (a 404, the session already gone, keeps them: a move made before it ended is read at the next `com.status`), also when a session is dropped on a live board (FAA-163): an unanswered one keeps
|
|
155
|
+
them, and the slot lapses in 60 s.
|
|
156
|
+
|
|
157
|
+
**One capability at a time (FAA-163).** At connect (and every reconnect) the responder starts with no slot. Each
|
|
158
|
+
capability's enroll or replace is settled on its own: a refusal of that capability (`capacity_exceeded`,
|
|
159
|
+
`invalid_arguments`, any code that is not about the board session) leaves it `retry`, asked again alone after 15 s,
|
|
160
|
+
30 s, doubling to 15 minutes, with one `board.slot.refused` event (warning; said again if it is refused again after
|
|
161
|
+
the slot was held); its accepted work waits `lapsed` and goes back
|
|
162
|
+
by a transfer once the slot is held. The capabilities after it are recovered as before. A refusal of the board session
|
|
163
|
+
(`unreachable`, `session-ended`, `not_available`, `not_authorized` and the other codes in `SESSION_CODES`) leaves the
|
|
164
|
+
rest `retry` too, says no `board.slot.refused`, and goes to the responder's fault, which says it by code:
|
|
165
|
+
`board.access.needed` for `not_authorized`, `authority_changed` and `credential` (the session is left),
|
|
166
|
+
`board.blocked` for a defect (`no-epoch`, `schema-unsupported`, `checkpoint`, `receipt-shape`,
|
|
167
|
+
`idempotency_conflict`), and `board.down` for the rest: `unreachable` keeps the session while a held slot's lease runs
|
|
168
|
+
(so a lost enroll of one capability is asked again at its next retry), `not_available` keeps it except at connect or recovery with no slot held,
|
|
169
|
+
and `session-ended`, `not-running` and the other hub codes drop it (as a fault during connect or recovery does) and
|
|
170
|
+
reconnect. A slot whose lease lapsed (`lease_expired`) holds
|
|
171
|
+
no lease for that decision.
|
|
147
172
|
|
|
148
173
|
## When the operator holds work (DEC-0079)
|
|
149
174
|
|
|
@@ -178,9 +203,16 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
178
203
|
{project, generation, moved?, asking?}}`). The record is written when a receipt's generation changes and before an
|
|
179
204
|
enroll or a replace that can change it; a live holder's renew (about every 20 s) cannot, and writes nothing. At a
|
|
180
205
|
clean leave (the service stopped; the session's DELETE released its slots) the record of a slot with no request left
|
|
181
|
-
in the checkpoint and not moved is forgotten
|
|
182
|
-
|
|
183
|
-
|
|
206
|
+
in the checkpoint and not moved is forgotten when this session confirmed its generation (held it, paused under the
|
|
207
|
+
operator's hold included, or read it unmoved in `com.status`) and no fenced call has put in doubt since: the slot is
|
|
208
|
+
free, and whoever takes it next is no move. An unconfirmed record stays, since the slot may have moved while the
|
|
209
|
+
service was down, and a move this session saw keeps its record, so a clean stop never undoes it. A paused or
|
|
210
|
+
retrying slot reads `com.status` before each new ask. A move made after the slot's last renew or check and before
|
|
211
|
+
the stop, with no call since, is not seen at the leave: its record is forgotten, and the next start reads the slot
|
|
212
|
+
as free and may enroll it once the other holder lets it go (FAA-187). A file without the record
|
|
213
|
+
reads as before; a kit before 0.53.0 reads the file, ignores the record and drops it at its first checkpoint write. So a service stopped cleanly with no request left in its checkpoint, its DELETE answered 2xx and every slot confirmed
|
|
214
|
+
in that session, rolled back to 0.52 and up again starts as on a first start. Otherwise (a 404, the idle wait running
|
|
215
|
+
out, a slot this session never confirmed, one left retrying before `com.status` answered for it included) its records stay, and the case is the crash case below. After a crash the record stays: if 0.52 then ran, enrolled the slot and
|
|
184
216
|
wrote nothing to the checkpoint, the next 0.53 start reads that enroll as a move; bring it back with the service's take-back action (below).
|
|
185
217
|
- An enroll or a replace is written into that record with its key before it is sent (`asking`). When its answer is
|
|
186
218
|
lost, the next connect (or the next enroll) sends the same call again under the same key; the board answers the
|
|
@@ -205,8 +237,8 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
205
237
|
holder, or the estate owner, reconciles it now). No `com.claim`, `com.renew`, `com.complete`, `com.enroll` or
|
|
206
238
|
`com.responder_replace` is sent for that capability again: not at recovery after a restart, and not when the new
|
|
207
239
|
holder's lease lapses, nor when com.status later names no generation for the slot (a recorded move stays until
|
|
208
|
-
take-back). Accepted requests stay
|
|
209
|
-
will hand them to the new holder). One `board.slot.moved` event (
|
|
240
|
+
take-back). Accepted requests stay `accepted` on the board, unanswered, until the slot is taken back (CT-175
|
|
241
|
+
will hand them to the new holder). One `board.slot.moved` event (warning, notify) says so, once, also across restarts
|
|
210
242
|
(the record keeps `moved`).
|
|
211
243
|
- A responder that never held the slot (a first start: no record, or a record of another Project), or whose record
|
|
212
244
|
equals the current generation, behaves as before: it enrolls, recovers and replaces its own lapsed work. A recorded or
|
|
@@ -216,18 +248,28 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
216
248
|
records its own generation. Only a replay the board refuses, or one that ran for the first time (`repeated: false`)
|
|
217
249
|
after the slot had moved past the record, reads as a move: the safe side; a slot such a replay took is left to lapse
|
|
218
250
|
unused. A generation written `2.0` is `2`.
|
|
219
|
-
- Only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record
|
|
251
|
+
- Only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record, and wins over a move
|
|
252
|
+
being read at the same time: the move is written under the responder's lock (Node: on the fence queue, after any fenced
|
|
253
|
+
call in flight) and only while the record is unchanged (FAA-158). Never edit the
|
|
220
254
|
checkpoint's `slots` record by hand to undo a move. In the same board
|
|
221
255
|
session the next tick enrolls once the other session lets the slot go (a live holder is waited for) and takes the work
|
|
222
|
-
left `lapsed` back with a transfer. A restart or reconnect before that, with accepted work
|
|
256
|
+
left `lapsed` back with a transfer. A restart or reconnect before that, with accepted work waiting for the slot, takes it
|
|
223
257
|
back at once by `com.responder_replace`, as a restart did before 0.53.0: the operator asked for the slot back, so the
|
|
224
258
|
other session is replaced. In recovery, a refusal that leaves no slot held reconnects; one that leaves some held keeps
|
|
225
|
-
the session and serves them. The kit gives it no route of its own
|
|
226
|
-
FAA-157): a
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`
|
|
230
|
-
|
|
259
|
+
the session and serves them. The kit gives it no route of its own. The reference service (`sample_service.py`) offers
|
|
260
|
+
it on its dashboard (FAA-157): a moved slot shows a Take back button that posts `POST /api/board/take-back` from the
|
|
261
|
+
operator's signed-in session with the `X-Fabric-Request` header; the service token alone is refused. Since 0.56.0 the
|
|
262
|
+
button has its tool, `board.take-back` with `{capability}`, declared under `extensions/actions/0.1` with gate
|
|
263
|
+
`relayed` and target `capability`, and a capability of the manifest like every tool the well-known lists
|
|
264
|
+
(`interop.well-known-capabilities`): an agent whose MCP client sets `_meta` calls it over `/mcp` with the operator's
|
|
265
|
+
decision in `_meta.decision` (a call without one is `operator-decision-required`); Claude Code and Codex send
|
|
266
|
+
arguments only, so until CT-125 they hand the operator the page as Fabric Dashboards' `open_link`. Either door records
|
|
267
|
+
the take-back as a `board.slot.returned` event (info, the capability as its subject) whose sentence says the route in
|
|
268
|
+
words and whose `data` holds `by`, `via` and the `caller` (none when the feed refuses the caller), never the decision's words; a repeat after a lost answer is answered `{capability, moved: false}`. A service built on the kit declares it as an operator action, a button with its tool ([Every button has a
|
|
269
|
+
tool](../SKILL.md), `fabric-actions/0.1`) with gate `relayed` (the operator's decision), never `none`, so a call
|
|
270
|
+
without the operator's relayed decision is refused; on a server that reads mandates (`agent_id`) it also names its effect class, for example
|
|
271
|
+
`change-policy` (DEC-0125); the operator reaches it through an agent that relays their decision in `_meta`, the
|
|
272
|
+
service's page or its elicitation (Claude Code and Codex, which cannot set `_meta`, wait for CT-125's forwarding), and the service names it in its docs; otherwise a moved slot stays moved. A move back to this service's own session is not
|
|
231
273
|
detected yet: the receiving side and the hand-over of accepted requests (contract CT-175) wait for Fabric's session
|
|
232
274
|
surface.
|
|
233
275
|
|
|
@@ -251,11 +293,19 @@ raises `directory-invalid` rather than answering; an unlisted Project may still
|
|
|
251
293
|
|
|
252
294
|
## Events, the probe and the live smoke
|
|
253
295
|
|
|
254
|
-
Events go to `emit(kind, level, text, notify=…, data=…)` (`JsonlEventLog.append` fits
|
|
296
|
+
Events go to `emit(kind, level, text, notify=…, subject=…, data=…)` (`JsonlEventLog.append` fits: every level is one
|
|
297
|
+
of its `info`, `notice`, `warning`, `error`; `subject` is `{type, id}`, the notice's request, else its capability, else
|
|
298
|
+
its episode (such as a block's cause), else the Project; in Python it goes only to an `emit` whose signature names a `subject`
|
|
299
|
+
parameter with no value bound, as `JsonlEventLog.append` does, and any other `emit`, one with only `**kwargs`, one with
|
|
300
|
+
a subject bound by `functools.partial` (which then stands) or one whose signature cannot be read, is called once, without
|
|
301
|
+
one, as before 0.56.0; the Node responder always passes it in its options, which an `emit` that does not read it ignores) with
|
|
302
|
+
`data {schema, value}`
|
|
255
303
|
checked by FAC-SEM-101 (no credential in data): `board.request.completed`, `board.request.refused`,
|
|
256
304
|
`board.request.skipped`, `board.request.reconciled`, `board.request.held`, `board.attempt.lost`,
|
|
257
|
-
`board.reconcile.needed`, `board.slot.contested`, `board.slot.moved`, `board.access.needed`, `board.blocked`,
|
|
258
|
-
`board.down`, each notifying episode said once.
|
|
305
|
+
`board.reconcile.needed`, `board.slot.contested`, `board.slot.moved`, `board.slot.refused`, `board.access.needed`, `board.blocked`,
|
|
306
|
+
`board.down`, each notifying episode said once, and `board.outcome.invalid` (warning) when a handler's outcome fails
|
|
307
|
+
its checks and is not sent. The operator's hold (below) adds `board.held`, `board.slot.held`,
|
|
308
|
+
`board.slot.released` and `board.request.waiting`. The reference service adds `board.slot.returned` at a take-back.
|
|
259
309
|
|
|
260
310
|
`check_service.py <id> [--board-kit <the service's fabric_board.py or fabric-board.mjs>]` rule **board.responder**:
|
|
261
311
|
`NOT_RUN` when the well-known `protocols` names no `fabric-project-comms` revision; `FAIL` for a revision this kit does
|
|
@@ -45,7 +45,12 @@ 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`)
|
|
48
|
+
host, port and path (`redact_url`); a URL that is not http or https keeps only its scheme (`htps:<redacted>`), and
|
|
49
|
+
one that does not parse as a URL is `<unparseable url>`. The path is not redacted (Node writes it as `new URL`
|
|
50
|
+
normalises it, Python as given), so a key never belongs in it. Python's `check_url` refuses a host
|
|
51
|
+
with a backslash, space, control character (C0, DEL or C1) or `%`, a URL with a space or ASCII control character before its
|
|
52
|
+
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
|
|
53
|
+
ASCII digits, all as kind `refused` before sending; it checks a host's shape, not its name. `redact_url` on Python reads a host holding `%` as `<unparseable url>`; Node writes it decoded. An error never carries a request header or body.
|
|
49
54
|
|
|
50
55
|
## fabric_openrouter
|
|
51
56
|
|
|
@@ -77,6 +82,7 @@ const answer = await client.chat(messages, { model, fallback, maxTokens: 800 });
|
|
|
77
82
|
| The fallback | required, of another vendor (the part before `/` differs) |
|
|
78
83
|
| Content | text only: a string, null, or parts that are all `{"type": "text"}`; an image, audio or file part is refused (`config`), because what it is billed cannot be bounded from its bytes |
|
|
79
84
|
| What the ceiling does not cover | the `provider` routing param can send a call to a provider priced above the catalog; two `Client` objects sharing one ledger each hold only their own calls in flight, so give an agent one client per key |
|
|
85
|
+
| The base URL | `base_url` (`baseUrl`), by default `https://openrouter.ai/api/v1`: checked by `check_url` when the client is built; ASCII whitespace and trailing `/` at its ends are trimmed (Node also trims every C0 control at the ends and removes a tab or line break anywhere, as `new URL` reads them); a base that is not a string is refused as `check_url` refuses it (`HttpError` kind `refused`, both twins); a query or a fragment is refused (kind `config`, both twins, before `check_url`). Python also refuses a space, a control or a non-ASCII character in the path (`check_url`'s `HttpError` kind `refused`); Node sends it percent-encoded, as `new URL` writes it (FAA-187 (18)) |
|
|
80
86
|
| Attribution | off unless an `attribution` setting is given (FAC-SEM-098, `fabric_usage.attribution_headers`) |
|
|
81
87
|
| Request body | `model`, `messages`, `max_tokens`, `usage: {include: true}` and your params (temperature, reasoning, response_format, tools); `stream`, `model`, `messages`, `max_tokens` and `usage` as params are refused |
|
|
82
88
|
|
|
@@ -124,6 +124,15 @@ page as Fabric Dashboards' `open_link`; both kits carry the same sentence. The g
|
|
|
124
124
|
`JwksCache` holds the estate's key set at most 10 minutes and, on a `kid` it does not hold, re-reads at most once
|
|
125
125
|
a minute (DL-15), and waits 10 s after a failed read before trying again; after 10 minutes without a successful read it answers nothing, and the verifier says
|
|
126
126
|
`issuer-unavailable`. It reads https, http on loopback only, or a file; a URL with credentials is refused. The
|
|
127
|
+
Python kit also refuses, https included, a URL holding a space or ASCII control character before its first `#` (it
|
|
128
|
+
reads the URL up to there, as Node does), or a character outside ASCII in its path or query (urllib would send it
|
|
129
|
+
and every read fail; a trailing ASCII space or line break is trimmed as urllib reads it; a Unicode space ending the
|
|
130
|
+
part before the first `#` is refused, as `check_url` refuses it, because Node keeps it percent-encoded (urllib would
|
|
131
|
+
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
|
|
132
|
+
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
|
|
133
|
+
65535, no host, and an empty userinfo. The Node kit accepts several of these as `new URL` reads them, among them a `%`
|
|
134
|
+
host decoded, a backslash as `/`, a tab or line break removed, an empty userinfo dropped and an extra slash before the
|
|
135
|
+
host skipped. The
|
|
127
136
|
Python kit verifies Ed25519 itself (RFC 8032, standard library only, strict as OpenSSL: `S` below the group order,
|
|
128
137
|
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
|
|
129
138
|
itself (`isSmallOrderKey`), because Node before 26 lets OpenSSL accept a signature under one.
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md
CHANGED
|
@@ -149,7 +149,8 @@ operator's own coding agents: Claude Code and Codex cannot set `_meta`, so until
|
|
|
149
149
|
a `relayed` gate turns "approve it" in chat into `operator-decision-required` with the page as the only route.
|
|
150
150
|
Until then a service keeps its tools as they answer today, or gates a `relayed` tool only where it offers its own
|
|
151
151
|
elicitation (the `elicited` hook). The one exception is the board responder's take-back action: it is declared
|
|
152
|
-
`relayed` now
|
|
152
|
+
`relayed` now (the reference service's `board.take-back`); an agent whose MCP client sets `_meta` calls it with the
|
|
153
|
+
operator's decision, and Claude Code and Codex reach it through the page until CT-125 (`board.md`). This is D8's reason, which keeps `input_actions` off, applied to tools.
|
|
153
154
|
|
|
154
155
|
**Parity (AP-1).** Every action the dashboard, app or page lets a person take (approve, reject, choose, edit, pause,
|
|
155
156
|
resume, cancel, retry, send, publish, restore, set a policy, any other) has a tool on the MCP surface with the same
|
|
@@ -201,8 +202,11 @@ well_known = fs.build_well_known(..., surfaces=surfaces, actions=server.action_t
|
|
|
201
202
|
`caller-missing`: an action-gated server must pass `tier`, `identify` or `authorize`,
|
|
202
203
|
since `handle` defaults to T2 and a T2 call names nobody without them). One fault reaches the caller by name:
|
|
203
204
|
`mandate-capability-missing` (DEC-0141 MJ-1), read as `operator-decision-required` with that `data.detail`.
|
|
204
|
-
`on_fault` hears it
|
|
205
|
-
|
|
205
|
+
`on_fault` hears it under the job's capability (the name the job was created under) both from `ctx.approval_step`
|
|
206
|
+
at a job's start, where the caller sees no refusal: the job waits `input_required`, its message naming the fault,
|
|
207
|
+
and at an answer through `fabric.job.get`, where the call is refused; at the start it hears any fault `job_step`
|
|
208
|
+
answers (also `now-unreadable` from the server's clock). A capability name is what `^[a-z][a-z0-9._-]{1,127}$`
|
|
209
|
+
matches (`reviews.approve`, never `reviewsApprove`); a job created under another name is read under that name.
|
|
206
210
|
- **Arguments with no canonical form** (a lone surrogate, a NaN or an infinity that Python's `json.loads` lets in,
|
|
207
211
|
nesting past the recursion limit) have no digest to bind: a relayed tool answers JSON-RPC `-32602`, never an exception.
|
|
208
212
|
- **The handler** reads `ctx.record` (`"caller"`, or `{by, via, caller}`) and writes it into the service's audit
|
|
@@ -305,7 +309,10 @@ server.add_tool(approve_tool, approve_reply, action=reply_action, target="reply_
|
|
|
305
309
|
step is kept in the job's private part (`request_input(..., steps=)`). An answer's `_meta.mandate` that covers that
|
|
306
310
|
step is then read when `input_actions=True`, beside a decision for the key. The rule alone is `job_step(mandate, ...)`
|
|
307
311
|
/ `jobStep({mandate, tier, now, self, capability, key, effect, callerAgent, spendUsd, project})`; Node's
|
|
308
|
-
`requestInput(jobId, inputRequests, statusMessage, {steps})` keeps the same private part.
|
|
312
|
+
`requestInput(jobId, inputRequests, statusMessage, {steps})` keeps the same private part. Node has no
|
|
313
|
+
`approvalStep`, so a Node service calls `jobStep` itself and passes `{}` when the start call's `_meta` carried
|
|
314
|
+
`mandate: null` (`Object.hasOwn(meta, 'mandate') && meta.mandate === null ? {} : meta.mandate`): `null` is no mandate
|
|
315
|
+
to `jobStep`, while the answer reads it as an unreadable one (FAA-178). Each `request_input`
|
|
309
316
|
replaces the kept steps, and an answer clears them: pass `steps=` again on every question whose answer a mandate may
|
|
310
317
|
cover. The answer's mandate is read under the job's own capability, as at the start (`action_gate(...,
|
|
311
318
|
mandate_capability=)` / `actionGate({mandateCapability})`), so the start and the answer agree whatever the tool is
|
|
@@ -328,7 +335,9 @@ lead, at, expiresAt})` (a day unless told, at most seven), hands it to the agent
|
|
|
328
335
|
`hand_on(mandate, callee, at, narrower=None)` / `handOn(mandate, callee, at, narrower)` (the chain gains the callee; a
|
|
329
336
|
wider scope, a fifth agent, an agent already in it or a time at the expiry is refused; `source` travels unchanged), and
|
|
330
337
|
sends it with `request_meta(traceparent, mandate=...)` / `requestMeta(traceparent, {mandate})`, which refuse one
|
|
331
|
-
`FAC-SEM-144` refuses. `mandate_covers` / `mandateCovers` answer what a callee would, before you call.
|
|
338
|
+
`FAC-SEM-144` refuses. `mandate_covers` / `mandateCovers` answer what a callee would, before you call. To cover a
|
|
339
|
+
job's approval steps, name the job's capability in `capabilities` (or leave `capabilities` out) and the steps' keys in
|
|
340
|
+
`actions`; never `fabric.job.get`, which covers no step of another capability (DEC-0141 MJ-1). Never write a
|
|
332
341
|
mandate from another agent's text, a page or a file; a callee hands on only what it received, narrowed.
|
|
333
342
|
|
|
334
343
|
A relay the operator's rule accepts (DEC-0126 OA-2, OA-3: one of his own Projects on his board, provenance `trusted`,
|
|
@@ -87,6 +87,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
87
87
|
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
88
88
|
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
89
89
|
| L4 | `fabric_board_link` | the board's link: the MCP session over the hub, the fsynced checkpoint, a claimed request as data |
|
|
90
|
+
| L4 | `fabric_board_slots` | the responder's slot records: the generation held, a move and its take-back, one capability's refusal retried alone (FAA-163) |
|
|
90
91
|
| L4 | `fabric_board_session` | the board responder: the work context, the fenced request, transfer and recovery |
|
|
91
92
|
| L4 | `fabric_board_fixture` | the scripted board the probe runs a responder against |
|
|
92
93
|
| L4 | `fabric_board` | the facade of the board connector ([board](board.md)) |
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -55,7 +55,7 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
|
|
|
55
55
|
|
|
56
56
|
| Id | Rule | Here | Kit (Python / Node) |
|
|
57
57
|
|---|---|---|---|
|
|
58
|
-
| LC-01 | **Quit reaches exit within a deadline.** Every trigger (`SIGTERM`, `SIGINT`, logout, the app's own quit) ends the process: within 5 s idle, within 10 s with work in flight, then a hard exit (
|
|
58
|
+
| LC-01 | **Quit reaches exit within a deadline.** Every trigger (`SIGTERM`, `SIGINT`, logout, the app's own quit) ends the process: within 5 s idle, within 10 s with work in flight, then a hard exit (an idle board responder waits 3 s at most for its session's DELETE, FAA-173; one with work can take up to that hard exit). Nothing starts after quit begins | [Shutdown](#shutdown) | `Drain` / `Drain` |
|
|
59
59
|
| LC-02 | **Every child has an owner, a deadline and a process group.** No subprocess without a timeout; on timeout or the parent's exit the whole group goes; no orphan survives. Locks and shared resources are released in `finally` | your own code | none |
|
|
60
60
|
| LC-03 | **A job is bounded and exclusive.** One copy holds `service.lock`; a scheduled job has a watchdog shorter than its interval, shares its lock with every manual entry point, bounds every network call and records each run's outcome. Long work is journalled before it starts | [One copy under a supervisor](#one-copy-under-a-supervisor) | `hold_single_instance` / `holdSingleInstance` |
|
|
61
61
|
| LC-04 | **Credentials are silent after setup.** No background prompt; a credential read that is denied or absent is cached and backs off until a person acts | [Directories](#directories) (secret files 0600) | none |
|