@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.83.1
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 +122 -3
- package/README.md +6 -3
- package/SECURITY.md +4 -3
- 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 +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- 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/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +37 -12
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +20 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +8 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +274 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +54 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +45 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +26 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +21 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +3 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +20 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +24 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +455 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +17 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +18 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +55 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +180 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +468 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +404 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +97 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +35 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +15 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +27 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +18 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +23 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +31 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +18 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +9 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +55 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +15 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +43 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +647 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +16 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +20 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +93 -93
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +37 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +390 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +3 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +492 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +365 -53
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +5 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +162 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/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/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The code checks of the support-agent recipe for a Node agent (FAA-264 item 10): checkReply, the gate the
|
|
2
|
+
// communication template runs on every final text, and what it reads (unsupported, scriptOf). The twin of
|
|
3
|
+
// support_agent.py's check_reply; both answer the cases in test/fixtures/support-check-reply-cases.json the same way.
|
|
4
|
+
// Python's Unicode classes are written out (\w as [\p{L}\p{N}_], \d as \p{Nd}, \b as lookarounds), since a JavaScript
|
|
5
|
+
// \w, \d and \b are ASCII. Node 20+, no dependency.
|
|
6
|
+
const W = '\\p{L}\\p{N}_';
|
|
7
|
+
const LETTER = '\\p{L}\\p{Nl}\\p{No}'; // Python's [^\W\d_]: a word character that is no decimal digit and no underscore
|
|
8
|
+
const DASHES = new RegExp(`[\\u2012\\u2014\\u2015\\u2e3a\\u2e3b\\ufe58]|--|(?<=[${LETTER}])[ \\u00a0][-\\u2013]\\s(?=[${LETTER}])`, 'u');
|
|
9
|
+
const CITATION = /\[KB:([A-Za-z0-9][A-Za-z0-9._-]{0,63})\]/g;
|
|
10
|
+
const URL_RUN = /https?:\/\/\S+/gu;
|
|
11
|
+
const CCTLD = ('ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bw by bz ca '
|
|
12
|
+
+ 'cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo '
|
|
13
|
+
+ 'fr ga gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je '
|
|
14
|
+
+ 'jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo '
|
|
15
|
+
+ 'mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt '
|
|
16
|
+
+ 'pw py qa re ro rs ru rw sa sb sc sd se sg sh si sk sl sm sn so sr ss st su sv sx sy sz tc td tf tg th tj tk '
|
|
17
|
+
+ 'tl tm tn to tr tt tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw');
|
|
18
|
+
const GTLD = ('com net org info biz edu gov mil int app dev xyz online site shop store tech cloud email link live news blog '
|
|
19
|
+
+ 'page help support pro club top vip one world today space website mobi name travel agency company services '
|
|
20
|
+
+ 'solutions digital network media studio design money finance bank pay refund claim click download win '
|
|
21
|
+
+ 'рф рус укр бел срб');
|
|
22
|
+
const TLDS = [...new Set(`${CCTLD} ${GTLD}`.split(' '))].sort((a, b) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0)).join('|');
|
|
23
|
+
const STOP = '[^\\s<>()"\'\\[\\]]';
|
|
24
|
+
const LINK = new RegExp(`(?:(?<![${W}])https?://|(?<![${W}@.-])www\\.)${STOP}+`
|
|
25
|
+
+ `|(?<![${W}@.\\-/])(?:[\\p{L}\\p{N}](?:[${W}-]{0,61}[\\p{L}\\p{N}])?\\.)+(?:${TLDS})(?![${W}-])(?::\\p{Nd}{1,5})?(?:[/?#]${STOP}*)?`, 'giu');
|
|
26
|
+
const EMAIL = new RegExp(`[${W}.+-]+@[${W}-]+(?:\\.[${W}-]+)+`, 'gu');
|
|
27
|
+
const PHONE = new RegExp(`(?<![${W}+])\\+?\\p{Nd}(?:[ ().-]{0,2}\\p{Nd}){6,}`, 'gu');
|
|
28
|
+
const CURRENCY = { $: 'usd', '€': 'eur', '£': 'gbp', '¥': 'jpy', '₽': 'rub', '₹': 'inr', dollar: 'usd', dollars: 'usd', euro: 'eur', euros: 'eur', pound: 'gbp', pounds: 'gbp' };
|
|
29
|
+
const CODES = 'usd|eur|gbp|jpy|rub|inr|chf|cad|aud|cny|uah|pln|try';
|
|
30
|
+
const UNIT = `[$\\u20ac\\u00a3\\u00a5\\u20bd\\u20b9]|(?<![${W}])(?:${CODES}|dollars?|euros?|pounds?)(?![${W}])|руб[${W}]*`;
|
|
31
|
+
const NUM = '\\p{Nd}+(?:[.,]\\p{Nd}+)*';
|
|
32
|
+
const AMOUNT = new RegExp(`(${UNIT})\\s?(${NUM})|(${NUM})\\s?(${UNIT})`, 'giu');
|
|
33
|
+
const NUMBER = new RegExp(NUM, 'gu');
|
|
34
|
+
const TRAILING = ".,;:!?'\")]";
|
|
35
|
+
export const SCRIPTS = Object.freeze([
|
|
36
|
+
['latin', [[0x41, 0x5A], [0x61, 0x7A], [0xC0, 0x24F], [0x1E00, 0x1EFF]]],
|
|
37
|
+
['cyrillic', [[0x400, 0x52F]]],
|
|
38
|
+
['greek', [[0x370, 0x3FF]]],
|
|
39
|
+
['hebrew', [[0x590, 0x5FF]]],
|
|
40
|
+
['arabic', [[0x600, 0x6FF], [0x750, 0x77F]]],
|
|
41
|
+
['devanagari', [[0x900, 0x97F]]],
|
|
42
|
+
['thai', [[0xE00, 0xE7F]]],
|
|
43
|
+
['cjk', [[0x3040, 0x30FF], [0x3400, 0x4DBF], [0x4E00, 0x9FFF], [0xAC00, 0xD7AF]]],
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** The script most of a text's letters are written in (citations and URLs left out), or null without letters. */
|
|
47
|
+
export function scriptOf(text) {
|
|
48
|
+
const counts = new Map();
|
|
49
|
+
for (const ch of text.replace(CITATION, ' ').replace(URL_RUN, ' ')) {
|
|
50
|
+
const code = ch.codePointAt(0);
|
|
51
|
+
const hit = SCRIPTS.find(([, ranges]) => ranges.some(([lo, hi]) => lo <= code && code <= hi));
|
|
52
|
+
if (hit) counts.set(hit[0], (counts.get(hit[0]) ?? 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
let best = null;
|
|
55
|
+
for (const [name, n] of counts) if (best === null || n > counts.get(best) || (n === counts.get(best) && name > best)) best = name;
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const currency = (unit) => {
|
|
60
|
+
const lowered = unit.toLowerCase();
|
|
61
|
+
return lowered.startsWith('руб') ? 'rub' : (CURRENCY[lowered] ?? lowered);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function linkKey(link) {
|
|
65
|
+
let key = link.replace(/^https?:\/\//i, '');
|
|
66
|
+
while (key && (TRAILING + '/').includes(key[key.length - 1])) key = key.slice(0, -1);
|
|
67
|
+
const cut = key.search(/[/?#]/);
|
|
68
|
+
return cut < 0 ? key.toLowerCase() : key.slice(0, cut).toLowerCase() + key.slice(cut);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rstrip = (text, chars) => { let end = text.length; while (end && chars.includes(text[end - 1])) end -= 1; return text.slice(0, end); };
|
|
72
|
+
|
|
73
|
+
function claims(text) {
|
|
74
|
+
const amounts = new Set();
|
|
75
|
+
for (const m of text.matchAll(AMOUNT)) amounts.add(JSON.stringify([currency(m[1] ?? m[4]), m[2] ?? m[3]]));
|
|
76
|
+
return {
|
|
77
|
+
url: new Set([...text.matchAll(LINK)].map((m) => linkKey(m[0]))),
|
|
78
|
+
email: new Set([...text.matchAll(EMAIL)].map((m) => rstrip(m[0], TRAILING).toLowerCase())),
|
|
79
|
+
phone: new Set([...text.matchAll(PHONE)].map((m) => m[0].replace(/\P{Nd}/gu, ''))),
|
|
80
|
+
amount: amounts,
|
|
81
|
+
number: new Set([...text.matchAll(NUMBER)].map((m) => m[0])),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** unsupported:<kind> for each kind of claim in the reply that the source text does not carry. */
|
|
86
|
+
export function unsupported(reply, source) {
|
|
87
|
+
const said = claims(reply);
|
|
88
|
+
const known = claims(source);
|
|
89
|
+
const problems = [];
|
|
90
|
+
for (const kind of ['url', 'email', 'phone', 'amount', 'number']) {
|
|
91
|
+
const missing = kind === 'phone'
|
|
92
|
+
? [...said[kind]].filter((c) => ![...known[kind]].some((k) => k.endsWith(c)))
|
|
93
|
+
: [...said[kind]].filter((c) => !known[kind].has(c));
|
|
94
|
+
if (missing.length) problems.push(`unsupported:${kind}`);
|
|
95
|
+
}
|
|
96
|
+
return problems;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What code can prove about a draft: {ok, problems, citations, reply}; the twin of support_agent.check_reply. */
|
|
100
|
+
export function checkReply(draft, question, given, { maxChars, sources = {} } = {}) {
|
|
101
|
+
const problems = [];
|
|
102
|
+
const cited = [...new Set([...draft.matchAll(CITATION)].map((m) => m[1]))];
|
|
103
|
+
if (!cited.length) problems.push('no-citation');
|
|
104
|
+
const unknown = cited.filter((c) => !given.includes(c));
|
|
105
|
+
if (unknown.length) problems.push(`unknown-citation:${unknown.join(',')}`);
|
|
106
|
+
if (DASHES.test(draft)) problems.push('dash');
|
|
107
|
+
const reply = draft.replace(CITATION, '').replace(/[ \t]+/g, ' ').replaceAll(' .', '.').replaceAll(' ,', ',').trim();
|
|
108
|
+
if (!reply) problems.push('empty');
|
|
109
|
+
else if ([...reply].length > maxChars) problems.push('too-long');
|
|
110
|
+
problems.push(...unsupported(reply, cited.filter((c) => given.includes(c)).map((c) => (Object.hasOwn(sources, c) ? sources[c] : '')).join('\n')));
|
|
111
|
+
const wanted = scriptOf(question);
|
|
112
|
+
const written = scriptOf(reply);
|
|
113
|
+
if (wanted && written && wanted !== written) problems.push(`script:${written}-not-${wanted}`);
|
|
114
|
+
return { ok: problems.length === 0, problems, citations: cited, reply };
|
|
115
|
+
}
|
|
@@ -58,7 +58,8 @@ _DASHES = re.compile(r"[\u2012\u2014\u2015\u2e3a\u2e3b\ufe58]|--|(?<=[^\W\d_])[
|
|
|
58
58
|
_CITATION = re.compile(r"\[KB:([A-Za-z0-9][A-Za-z0-9._-]{0,63})\]")
|
|
59
59
|
_WORD = re.compile(r"\w+", re.UNICODE)
|
|
60
60
|
_URL = re.compile(r"https?://\S+")
|
|
61
|
-
_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
|
|
61
|
+
_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}") # a message id
|
|
62
|
+
_ENTRY_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}") # a knowledge id: exactly what _CITATION can name
|
|
62
63
|
# What a reply may carry only when the entries it cites carry it (D6): a link, an address, a phone number, an amount,
|
|
63
64
|
# a number. The customer's message never counts: an instruction hidden in it is exactly what this refuses.
|
|
64
65
|
# A bare host is a link only when its last label is a top-level domain people write in prose: every two-letter
|
|
@@ -154,8 +155,10 @@ class Knowledge:
|
|
|
154
155
|
def __init__(self, entries: List[Mapping[str, Any]]):
|
|
155
156
|
self.entries: Dict[str, Dict[str, Any]] = {}
|
|
156
157
|
for entry in entries:
|
|
157
|
-
if not isinstance(entry, dict) or not isinstance(entry.get("id"), str) or not
|
|
158
|
-
|
|
158
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("id"), str) or not _ENTRY_ID.fullmatch(entry["id"]):
|
|
159
|
+
# the citation's own grammar (FAA-264): an id with a colon or over 64 characters could never be cited
|
|
160
|
+
raise RecipeError("every knowledge entry has an id a citation can name: letters, digits, '.', '_' or "
|
|
161
|
+
"'-', up to 64 (map one that has a colon)")
|
|
159
162
|
if not isinstance(entry.get("answer"), str) or not entry["answer"].strip():
|
|
160
163
|
raise RecipeError("knowledge entry %s has no answer" % entry["id"])
|
|
161
164
|
if "question" in entry and not isinstance(entry["question"], str):
|
|
@@ -452,10 +455,52 @@ def prune(state_dir: Path, keep_days: int, *, now: Optional[datetime] = None) ->
|
|
|
452
455
|
return removed
|
|
453
456
|
|
|
454
457
|
|
|
458
|
+
def check_policy(policy: Mapping[str, Any]) -> Dict[str, List[str]]:
|
|
459
|
+
"""Categories only a person handles (refunds, account recovery, legal), each a list of words or phrases; a message
|
|
460
|
+
that holds one is routed to a person before the lookup and the model, so a refund is never a knowledge gap."""
|
|
461
|
+
checked: Dict[str, List[str]] = {}
|
|
462
|
+
for category, phrases in dict(policy).items():
|
|
463
|
+
if not isinstance(category, str) or not _ENTRY_ID.fullmatch(category):
|
|
464
|
+
raise RecipeError("a policy category is a short id (letters, digits, '.', '_' or '-')")
|
|
465
|
+
if not isinstance(phrases, list) or not phrases or not all(isinstance(p, str) and p.strip() for p in phrases):
|
|
466
|
+
raise RecipeError("policy %s lists the words that route a message to a person" % category)
|
|
467
|
+
checked[category] = [p.strip().lower() for p in phrases]
|
|
468
|
+
return checked
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def policy_route(policy: Mapping[str, List[str]], text: str) -> Optional[str]:
|
|
472
|
+
"""The first category whose phrase the message holds (whole words, any case), or None."""
|
|
473
|
+
lowered = " %s " % " ".join(_WORD.findall(text.lower()))
|
|
474
|
+
for category, phrases in policy.items():
|
|
475
|
+
if any(" %s " % " ".join(_WORD.findall(p)) in lowered for p in phrases):
|
|
476
|
+
return category
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
_REDACT = (
|
|
481
|
+
(re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+"), "[email]"),
|
|
482
|
+
(re.compile(r"(?<!\w)(?:\d[ -]?){13,19}(?!\w)"), "[card]"),
|
|
483
|
+
(re.compile(r"(?<![\w+])\+?\d[\d ()-]{6,}\d(?!\w)"), "[number]"),
|
|
484
|
+
(re.compile(r"[A-Za-z0-9+/=_-]{32,}"), "[secret]"),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def redact(text: str) -> str:
|
|
489
|
+
"""The customer's text as the outbox keeps it: an address, a card or phone number, and a long key-shaped run
|
|
490
|
+
replaced by a marker, so a person reading the queue never reads a code or a key the customer pasted."""
|
|
491
|
+
for pattern, marker in _REDACT:
|
|
492
|
+
text = pattern.sub(marker, text)
|
|
493
|
+
return text
|
|
494
|
+
|
|
495
|
+
|
|
455
496
|
class SupportAgent:
|
|
456
497
|
def __init__(self, *, knowledge: Union[Knowledge, "KnowledgeSource"], client: Any, model: str, fallback: str, state_dir: Path,
|
|
457
498
|
max_tokens: int = 600, max_chars: int = 1200, lookup_limit: int = 3,
|
|
458
|
-
on_handoff: Optional[Callable[[Dict[str, Any]], None]] = None
|
|
499
|
+
on_handoff: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
500
|
+
policy: Optional[Mapping[str, Any]] = None):
|
|
501
|
+
if fabric_openrouter.vendor(model) and fabric_openrouter.vendor(model) == fabric_openrouter.vendor(fallback):
|
|
502
|
+
raise RecipeError("the fallback is a model of another vendor than %s (refused at load, FAA-116)" % model)
|
|
503
|
+
self.policy = check_policy(policy or {})
|
|
459
504
|
self.knowledge, self.client = knowledge, client
|
|
460
505
|
self.model, self.fallback = model, fallback
|
|
461
506
|
self.state_dir = Path(state_dir)
|
|
@@ -492,6 +537,8 @@ class SupportAgent:
|
|
|
492
537
|
# The outbox keeps the customer's own text until it is pruned: see `prune` and the README's retention note.
|
|
493
538
|
fabric_paths.ensure_private_dir(self.state_dir)
|
|
494
539
|
kept = {k: v for k, v in decision.items() if k != "draft"} # a person reads the question and the reasons
|
|
540
|
+
if isinstance(kept.get("question"), str):
|
|
541
|
+
kept["question"] = redact(kept["question"]) # what a person reads carries no code, key, card or address
|
|
495
542
|
line = json.dumps(kept, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
496
543
|
_append_line(self.state_dir / "handoffs.jsonl", line.encode("utf-8"))
|
|
497
544
|
|
|
@@ -509,6 +556,9 @@ class SupportAgent:
|
|
|
509
556
|
rewrites the draft once with the refusal's reasons: a pass is a reply with `repaired: true`, and a repair that
|
|
510
557
|
does not end in a reply (a second refusal, a model `HANDOFF`, no knowledge on the retry; not a `model:*`
|
|
511
558
|
outage) a hand-off with `fallback: true` (the channel-agent standard's 4.4 and 13.2, FAA-197)."""
|
|
559
|
+
if getattr(self._local, "in_handoff", False): # first, so a bad input from on_handoff is a ReentryError too
|
|
560
|
+
raise ReentryError("answer() was called from on_handoff: queue the hand-off and call answer() again after "
|
|
561
|
+
"this call returns")
|
|
512
562
|
if not isinstance(message_id, str) or not _ID.fullmatch(message_id):
|
|
513
563
|
raise RecipeError("message id: letters, digits, '.', '_', ':' or '-', up to 128")
|
|
514
564
|
if not isinstance(text, str) or not text.strip():
|
|
@@ -518,9 +568,6 @@ class SupportAgent:
|
|
|
518
568
|
"decision or the on_handoff copy (a repeat returns no draft, and a fallback had its repair)")
|
|
519
569
|
if repair is not None and repair.get("messageId") == message_id:
|
|
520
570
|
raise RecipeError("repair= runs under a new message id: the refused one returns its stored decision")
|
|
521
|
-
if getattr(self._local, "in_handoff", False):
|
|
522
|
-
raise ReentryError("answer() was called from on_handoff: queue the hand-off and call answer() again after "
|
|
523
|
-
"this call returns")
|
|
524
571
|
text = re.sub("[\ud800-\udfff]", "\ufffd", text) # a lone surrogate (bytes a terminal could not decode) is kept, not fatal
|
|
525
572
|
with self._lock: # one decision per message id, in this process; the service's instance lock covers others
|
|
526
573
|
done = self._log.get(message_id)
|
|
@@ -540,6 +587,9 @@ class SupportAgent:
|
|
|
540
587
|
def _decide(self, message_id: str, text: str, task_run_id: Optional[str],
|
|
541
588
|
repair: Optional[Mapping[str, Any]] = None) -> Dict[str, Any]:
|
|
542
589
|
base = {"messageId": message_id, "citations": [], "costUsd": None}
|
|
590
|
+
routed = policy_route(self.policy, text)
|
|
591
|
+
if routed is not None: # a person's category goes to a person before any lookup or model call (FAA-116)
|
|
592
|
+
return dict(base, action="handoff", reasons=["policy:" + routed])
|
|
543
593
|
knowledge = self.knowledge.current() if isinstance(self.knowledge, KnowledgeSource) else self.knowledge
|
|
544
594
|
entries = knowledge.lookup(text, self.lookup_limit)
|
|
545
595
|
if not entries:
|
|
@@ -571,9 +621,10 @@ class SupportAgent:
|
|
|
571
621
|
def from_config(config_path: Path, *, on_handoff: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
572
622
|
**overrides: Any) -> SupportAgent:
|
|
573
623
|
"""An agent from a config file (see config.example.json): paths are relative to the config's folder. `on_handoff`
|
|
574
|
-
takes each hand-off in place of the outbox. Other keywords go to the model client
|
|
575
|
-
|
|
576
|
-
`
|
|
624
|
+
takes each hand-off in place of the outbox. Other keywords go to the model client as they are: the eleven the
|
|
625
|
+
config does not set (`base_url`, `attribution`, `unmeasured_call_usd`, `deadline_s`, `retries`, `environ`,
|
|
626
|
+
`transport`, `sleep`, `clock`, `now`, `rand`); the six it always sets (`ceiling_usd`, `window`, `ledger`, `prices`,
|
|
627
|
+
`key_env`, `project_id`) are not keywords here. `policy` in the config routes a person's categories first."""
|
|
577
628
|
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
578
629
|
home = Path(config_path).resolve().parent
|
|
579
630
|
state = (home / config.get("stateDir", "state")).resolve()
|
|
@@ -593,7 +644,8 @@ def from_config(config_path: Path, *, on_handoff: Optional[Callable[[Dict[str, A
|
|
|
593
644
|
knowledge = Knowledge.load(home / config["knowledge"])
|
|
594
645
|
return SupportAgent(knowledge=knowledge, client=client, model=config["model"],
|
|
595
646
|
fallback=config["fallback"], state_dir=state, max_tokens=int(config.get("maxTokens", 600)),
|
|
596
|
-
max_chars=int(config.get("maxChars", 1200)), on_handoff=on_handoff
|
|
647
|
+
max_chars=int(config.get("maxChars", 1200)), on_handoff=on_handoff,
|
|
648
|
+
policy=config.get("policy"))
|
|
597
649
|
|
|
598
650
|
|
|
599
651
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# A2A: serving a card and the binding, calling a peer
|
|
2
|
+
|
|
3
|
+
A service that other agents reach over the open A2A protocol (version 1.0, the contract's A2A peer profile) serves a
|
|
4
|
+
public Agent Card, answers A2A's JSON-RPC binding, and may call other A2A agents. The kit's module is
|
|
5
|
+
`fabric_a2a_server.py` with its Node twin `fabric-a2a-server.mjs` (FAA-10); `fabric_a2a.py` reads the card and the
|
|
6
|
+
well-known member (DEC-0122), and `check_service_a2a.py` holds the probe's A2A rules.
|
|
7
|
+
|
|
8
|
+
## The card
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from fabric_a2a import a2a_surface
|
|
12
|
+
from fabric_a2a_server import agent_card
|
|
13
|
+
|
|
14
|
+
card = agent_card(name="Example Agent", description="Drafts notes.", version="0.2.0",
|
|
15
|
+
url="http://127.0.0.1:47000/a2a/v1", capabilities=[{"name": "notes.draft", "description": "Drafts a note."}],
|
|
16
|
+
security_schemes={"own": {"apiKeySecurityScheme": {"location": "header", "name": "X-Example-Key"}}})
|
|
17
|
+
# serve card at GET /.well-known/agent-card.json, without the service token; declare it:
|
|
18
|
+
well_known["surfaces"]["a2a"] = a2a_surface()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- Each capability is one `AgentSkill`; its id is the capability's name, the id the provider manifest's A2A profile
|
|
22
|
+
lists in `skillIds`.
|
|
23
|
+
- One interface: the JSON-RPC binding at `url`, `protocolVersion` 1.0. On a local placement the URL is on the service's
|
|
24
|
+
loopback origin; on a remote one it is https.
|
|
25
|
+
- `agent_card` runs `agent_card_findings` before it returns, so a card that names a member about the running process,
|
|
26
|
+
holds a value shaped like a credential, names the descriptor's token header as its scheme, or marks a Fabric
|
|
27
|
+
extension required is never served.
|
|
28
|
+
- **The interface never takes the descriptor's token** (DEC-0122 item 3). It has its own scheme (an API key of its own,
|
|
29
|
+
OAuth, or none on a loopback interface trusted under HA-3), and the service's `authorize` turns the request's headers
|
|
30
|
+
into a caller.
|
|
31
|
+
|
|
32
|
+
## The binding
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from fabric_a2a_server import A2ABinding, agent_message, text_of
|
|
36
|
+
|
|
37
|
+
def handler(start): # a new message
|
|
38
|
+
if text_of(start.message) == "ping":
|
|
39
|
+
return agent_message("pong") # a reply with no task
|
|
40
|
+
start.start("notes.draft") # or a task: a job in the interop job store
|
|
41
|
+
run_in_background(start.task_id) # the service moves the job: working, request_input, complete, fail
|
|
42
|
+
|
|
43
|
+
binding = A2ABinding(jobs, handler, skills=["notes.draft"], authorize=lambda headers: keys.get(headers.get("x-example-key")))
|
|
44
|
+
status, body = binding.handle(request_body_bytes, request_headers) # POST /a2a/v1
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| A2A 1.0 | The kit |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `SendMessage` | the handler replies (`{message}`) or starts a job (`{task}`); a message with `taskId` answers an input-required task |
|
|
50
|
+
| `SendStreamingMessage` | `binding.stream(params, caller, rid=...)`: server-sent events, the task first, then a `statusUpdate` at each change, ending at a terminal or input-required state |
|
|
51
|
+
| `GetTask` | the job as a Task |
|
|
52
|
+
| `CancelTask` | `JobStore.cancel`; an ended task answers -32002 |
|
|
53
|
+
| task id | the job id; only the caller that started it reads, answers or cancels it (any other gets -32001) |
|
|
54
|
+
| `TASK_STATE_WORKING`, `_INPUT_REQUIRED`, `_COMPLETED`, `_FAILED`, `_CANCELED` | job `working`, `input_required`, `completed`, `failed`, `cancelled` |
|
|
55
|
+
| an input-required question | the job's elicitation requests in the status message's data part; the answer is a data part `{inputResponses}` as `fabric.job.get` takes it, or plain text for a question of one field |
|
|
56
|
+
| the result | the job's result envelope as the artifact `result`, one data part |
|
|
57
|
+
| a failure | the status message names the job's error message and its code in a data part |
|
|
58
|
+
|
|
59
|
+
JSON-RPC codes follow A2A 1.0 section 5.4: -32001 task not found, -32002 not cancelable, -32004 unsupported, -32009 an
|
|
60
|
+
`A2A-Version` header that is not 1.0; -32700, -32600, -32601 and -32602 are JSON-RPC's own. A caller `authorize`
|
|
61
|
+
refuses gets HTTP 401; a body over 1 MiB gets 413.
|
|
62
|
+
|
|
63
|
+
## Calling a peer
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from fabric_a2a_server import A2AClient
|
|
67
|
+
|
|
68
|
+
client = A2AClient(["https://peer.example"], credential_env="EXAMPLE_PEER_KEY")
|
|
69
|
+
peer = client.discover("https://peer.example") # {card, url}
|
|
70
|
+
answer = client.call(peer["url"], "Draft a note about Monday.")
|
|
71
|
+
client.get(peer["url"], answer["task"]["id"]); client.cancel(peer["url"], answer["task"]["id"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Only the operator's origins.** The client reaches an origin in its list, and the interface the card names must be
|
|
75
|
+
on one too.
|
|
76
|
+
- **The key from the environment.** `credential_env` names a variable that `use_secret.py run --vault-only <project>
|
|
77
|
+
<NAME> -- <command>` fills; the client never reads a key file beside the agent, and an error never carries the key.
|
|
78
|
+
- **Bounded.** Each call has a deadline (30 s) and a size cap (1 MiB) through `fabric_http`, follows no redirect and
|
|
79
|
+
never retries a `SendMessage`. A plain `http://` URL is refused unless it is loopback (and then its listener must be
|
|
80
|
+
this account's) or the client is built with `insecure=True`.
|
|
81
|
+
|
|
82
|
+
## The probe
|
|
83
|
+
|
|
84
|
+
`check_service_a2a.py <origin> [--remote] [--key-env NAME] [--probe-text TEXT]` reads `surfaces.a2a`, the card,
|
|
85
|
+
the interface and the binding's refusals (`a2a.surface`, `a2a.card`, `a2a.interface`, `a2a.task-not-found`, `a2a.cancel-not-found`,
|
|
86
|
+
`a2a.method-not-found`); `a2a.task-states` runs only with a message the service names as safe to send, since any
|
|
87
|
+
message may start real work. An interface that answers 401 without `--key-env` makes the binding rows NOT_RUN.
|
|
88
|
+
|
|
89
|
+
It is a command of its own, not a part of `check_service.py`: that probe's `a2a.card` reads the public card with no
|
|
90
|
+
Authorization header and never calls the card's interfaces (D5), while this one calls the interface with the
|
|
91
|
+
interface's own key. Run both for a service that serves A2A.
|
|
92
|
+
|
|
93
|
+
## Not here yet
|
|
94
|
+
|
|
95
|
+
- The interop test against a live Hermes A2A endpoint (`a2a_discover`, `a2a_call`): it waits for a Hermes A2A endpoint
|
|
96
|
+
reachable on this machine (FAA-10).
|
|
97
|
+
- The sample service serving both MCP and A2A.
|
|
98
|
+
- The REST and gRPC bindings, push notifications and `ListTasks`.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md
CHANGED
|
@@ -48,6 +48,16 @@ repository-relative path at a commit, an eval report). A decision without a numb
|
|
|
48
48
|
| building-fabric-services | at each release | contribute the release's measured wins as success cases and its new technical decisions |
|
|
49
49
|
| working-with-fabric | the retro after work | a lesson with a measured effect is also a success case: contribute it beside the lesson it records |
|
|
50
50
|
|
|
51
|
+
## A template and the communication prompt
|
|
52
|
+
|
|
53
|
+
For an agent that talks with people on a channel, the database's template and creating-fabric-agents' communication
|
|
54
|
+
agent prompt (`references/communication-agent-template.md`) are read together, never as two templates (the database's
|
|
55
|
+
own rule). Where they disagree, the template wins, in every part and not only the first-release ones: it is the merged
|
|
56
|
+
practice of the agents of its kind. A part the prompt carries and the template lacks (a first-release part, a check, a
|
|
57
|
+
pipeline step) is built too, unless the template rules it out, and goes back to the database as feedback on that
|
|
58
|
+
template (`agents-db.contribute`); the completion report names each such part. Without a template for the kind, the
|
|
59
|
+
prompt is the recipe.
|
|
60
|
+
|
|
51
61
|
## Recording the template
|
|
52
62
|
|
|
53
63
|
An agent built from a template declares it: `provenance.template` with its id and version, so a later template
|
|
@@ -121,7 +121,7 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
121
121
|
any other id as written. The mandate is a block fenced ` ```json `, the opening and the closing fence each at the start
|
|
122
122
|
of its own line, with no indent (lowercase; an indented fence, as inside a list item, is not read either; ` ````json `, ` ```JSON ` and `~~~` are not read), whose value has `kind: "mandate/1"`; any other JSON
|
|
123
123
|
block, a decision block included, is not read, with no warning. A fence quoted inside another fenced block is still
|
|
124
|
-
read: quote an example in prose, not in a fence. Two `mandate/1` blocks, whoever their chains end in, or one whose text holds `mandate/1` and is
|
|
124
|
+
read: quote an example in prose, not in a fence. Two `mandate/1` blocks, whoever their chains end in, or one whose text holds `mandate/1` (its slash plain or JSON-escaped, `\/` or `\u002f`, FAA-196 (13)) and is
|
|
125
125
|
not JSON (`NaN` included), read as none (a broken block without `mandate/1` is skipped, and a mandate beside it
|
|
126
126
|
is still read), and the responder emits `board.request.mandate_unread` (a warning) with the
|
|
127
127
|
request id and its `reason`, `mandate-blocks` or `mandate-unreadable`: the block holds his words, which reach no event, log or
|
|
@@ -131,6 +131,10 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
131
131
|
than 4300 digits on every Python (the kit refuses it there, as `int_max_str_digits` does from 3.11 and
|
|
132
132
|
the 3.8.14, 3.9.14 and 3.10.7 security releases; a limit configured
|
|
133
133
|
lower, `PYTHONINTMAXSTRDIGITS` on a Python that has one, refuses an integer longer than it).
|
|
134
|
+
- A request transferred to a new session runs again from a fresh `com.get`. Its mandate is read only when the message
|
|
135
|
+
read back carries the digest the claim took and names this request, capability and Project, as the first claim
|
|
136
|
+
checks; otherwise it is worked without one, and `board.request.mandate_unread` names `digest-mismatch` or
|
|
137
|
+
`not-addressed` (FAA-196 (1)).
|
|
134
138
|
- The handler gets `ctx.mandate` (the one block without `source`, or None) and `ctx.mandate_covers(call)` (Node
|
|
135
139
|
`ctx.mandateCovers(call)`, `call` being `{capability, action?, effect, spendUsd?, project?}`: pass `project` when the work
|
|
136
140
|
acts in one, since a call naming none is not limited by the mandate's `projects`; both are read failing closed, as
|
|
@@ -145,7 +149,12 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
145
149
|
spend; all five are read up front, whatever the scope limits, so a member that raises raises to the handler (in
|
|
146
150
|
Python a `KeyError` reads as not named), and a Python mapping's default, a `defaultdict`'s
|
|
147
151
|
included, is read as named, the `defaultdict` gaining the key): `mandate_covers` with `tier: "T1"`, `self` the service's `agent_id`, no `callerAgent` (the
|
|
148
|
-
board names a Project's principal, not an agent id the chain can bind, so the chain's end alone binds it)
|
|
152
|
+
board names a Project's principal, not an agent id the chain can bind, so the chain's end alone binds it), unless the
|
|
153
|
+
service passes `identify_agent=` (Node `identifyAgent`, FAA-135): a function of the request's sender (`{project,
|
|
154
|
+
principal}`, as the board verified it) answering the agent id the service authenticates for it, or None; asked only
|
|
155
|
+
when a mandate travelled, its agent id must be the one before `agent_id` in the chain, and an id the registry
|
|
156
|
+
refuses, an answer that is not a string or a hook that raises fails closed (`mandate-not-for-this-call`), as
|
|
157
|
+
[interop](interop.md) reads `identify_agent`; and the
|
|
149
158
|
responder's wall clock (`wall=`: seconds, `time.time` by default; Node milliseconds, `Date.now`; one that returns a value
|
|
150
159
|
that is not a finite number answers `{"fault": "now-unreadable"}`; a clock that raises passes its error to the handler). It answers `{"ok": True, "recordAs": {by, via: "mandate", task,
|
|
151
160
|
chain, caller}}` (`caller` the sender Project, as MD-3 records it; a sender that names no Project is never read), a
|
|
@@ -180,7 +189,7 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
180
189
|
| `authority_changed`, `not_authorized` | stop claiming; one `board.access.needed` event (notify) saying what to ask for (except `not_authorized` on the move-back claim of a moved slot, read as another session's slot, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)) |
|
|
181
190
|
| `fenced` on enroll or claim | another live holder: wait for it to lapse; never replace it on its own initiative. When this responder recorded holding the slot, it first reads `com.status`: a newer generation means the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155), and it is not taken again even after that holder lapses |
|
|
182
191
|
| lost after an effect began | keep the entry as `unknown`; `observe(entry)` is asked to settle it with `com.reconcile` |
|
|
183
|
-
| Fabric down, hub missing | bounded backoff (2 s doubling to 5 minutes, jittered), one `board.down` event, no spinning |
|
|
192
|
+
| Fabric down, hub missing | bounded backoff (2 s doubling to 5 minutes, jittered), one `board.down` event, no spinning; a board that flaps is said down once until it stays up `DOWN_QUIET_SECONDS` (300 s, FAA-187 (22)) |
|
|
184
193
|
| one call without an answer while the lease holds | `board.down`, but the session and the running work stay; the call goes again, with the same key, before the lease lapses |
|
|
185
194
|
| `revision_conflict` on `com.renew` or `com.progress` | they store nothing: the revision is read again (`com.get`) and the call sent once more; a second one loses the attempt |
|
|
186
195
|
| `idempotency_conflict` | a key sent with other content is a defect, not an outage: `board.blocked` (notify) naming it, and the session is left |
|
|
@@ -311,10 +320,10 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
311
320
|
Fabric release through v0.3.19 (2026-11-11) offers the operator's move (the control is CT-174's in Fabric, its agent twin CT-181 in the contract, then CT-125),
|
|
312
321
|
so today every move is another session taking the slot and take-back is the route back; the check below is ready
|
|
313
322
|
for the move. While
|
|
314
|
-
a slot is moved the responder reads `com.status` once a renewal (20 s) and, when the slot's generation is past the
|
|
323
|
+
a slot is moved the responder reads `com.status` once a renewal (20 s; one read a tick for every moved slot, FAA-194 (5)) and, when the slot's generation is past the
|
|
315
324
|
recorded move, sends one `com.claim` under a fresh key, as PR-2's "Learning the slot is its own" says (FAA-165,
|
|
316
325
|
0.60.0); the move itself drops the claim key of that capability, whose receipt the move made dead. A claim that
|
|
317
|
-
lands is the slot held again: the record loses `moved`, the request it handed out is worked (with none, its own
|
|
326
|
+
lands is the slot held again (`board.slot.returned`, info, says so; FAA-194 (8)): the record loses `moved`, the request it handed out is worked (with none, its own
|
|
318
327
|
enroll renews the lease at once), accepted work left `lapsed` comes back with a transfer, and a later move is said
|
|
319
328
|
again. `lease_expired` (the move reached this session and its lease lapsed first) enrolls again. `held` asks again
|
|
320
329
|
at the next renewal. `idempotency_window_expired` or `capacity_exceeded` asks again at the next renewal under a
|
|
@@ -323,7 +332,7 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
323
332
|
recorded as `moved` at that generation, so it is not asked about twice. A claim whose answer was lost (`unreachable`) is asked again under the same key at the next renewal, so a claim that
|
|
324
333
|
landed is not stranded; until then it counts as at most a lease from that key's first send (a fresh key starts
|
|
325
334
|
its own, and it counts only while more than the 10 s margin of that lease is left, then no more, as a lapsed slot), so a second lost answer keeps
|
|
326
|
-
the session (
|
|
335
|
+
the session (as any call without an answer does while a lease holds: the failure table above; FAA-194 (34)). A replayed receipt from a generation older than `com.status` shows (a move away and back meanwhile) is
|
|
327
336
|
dead: its key is dropped and a fresh key asks at the next renewal. A take-back drops the capability's claim key too, so a request a move-back claim handed out while the take-back raced
|
|
328
337
|
it waits for its attempt's lease (60 s) before it is claimed again (a take-back that lands before the move-back check makes its claim key
|
|
329
338
|
leaves that key to the claim loop, which works the request at once). Nothing is asked while the responder stops. It is never an enroll or a replace on a slot another session holds, and it waits
|
|
@@ -405,11 +414,13 @@ Python (`fabric_hub`): `fabric_root(platform, env)`, `locate_hub(root=None, aliv
|
|
|
405
414
|
`request_board_access(agent_id, project_id, capabilities, reason)`, `poll_access(request_id, poll_secret,
|
|
406
415
|
credential_file)`, `join_board(project_id, credential_file) -> BoardGrant`, `call_door(tool, args)`,
|
|
407
416
|
`call_binding(tool, args, credential_file)`; `pid_alive(pid)` answers True, False, or None for another OS account's
|
|
408
|
-
process (not verified); `
|
|
417
|
+
process (not verified); on Windows it asks OpenProcess for the access Node's `process.kill(pid, 0)` asks (libuv's
|
|
418
|
+
terminate, query and synchronize), so an elevated process of the same user reads None in both kits, as another
|
|
419
|
+
account's does, where limited query access alone would have read it as running (FAA-323); `Backoff(first=2, cap=300, jitter=0.2, floor=1)`; `HubError(code,
|
|
409
420
|
message, retryable, data)`.
|
|
410
421
|
|
|
411
422
|
Python (`fabric_board`): `Responder(*, hub, project_id, capabilities, credential_file, checkpoint_file, handler,
|
|
412
|
-
admit, observe=None, emit=None, service, now, rand, spawn, session_factory, agent_id=None, mandate_from=None, wall=time.time)`
|
|
423
|
+
admit, observe=None, emit=None, service, now, rand, spawn, session_factory, agent_id=None, mandate_from=None, wall=time.time, identify_agent=None, links=None)`
|
|
413
424
|
with `step()`, `run(stop, drain)`, `shutdown(drain)`, `take_back(capability)`; `WorkContext.progress(note)`,
|
|
414
425
|
`.begin_effect(summary)`, `.stopped`, `.mandate`, `.mandate_covers(call)`; `PERSONAL_BOARD`; `BoardSession(grant)`;
|
|
415
426
|
`Checkpoint(path)`; `comms_canonical(value)`, `comms_digest(operation, value)`, `receipt_problems(value, op)`,
|
|
@@ -420,4 +431,4 @@ effect_began)`, `self_check()`; `BoardError`, `Stopped`.
|
|
|
420
431
|
Node: the same names in camelCase (`locateHub(root, {alive, estate, now})`, `new HubClient(root, {alive, timeout,
|
|
421
432
|
estate, post})`, `new Responder({...})` with async `step()`, `run({signal, drain})`, `shutdown(drain)`,
|
|
422
433
|
`takeBack(capability)`; `await ctx.progress(note)`, `await ctx.beginEffect(summary)`, `ctx.mandate`, `ctx.mandateCovers(call)`;
|
|
423
|
-
the options `agentId`, `mandateFrom
|
|
434
|
+
the options `agentId`, `mandateFrom`, `wall` (milliseconds), `identifyAgent` and `links`; `selfCheck()`). Both kits read and write the same checkpoint file. `links` maps a notify kind to the page on the service that resolves it (`{"board.slot.moved": "/#board"}`, a path, as `make_event` takes one): a notice that notifies carries it as `link`, so the banner opens that page; give it only with an emit that takes `link` (`JsonlEventLog.append` does; FAA-187 (21)). A Node emit receives `{notify, subject, link?, data?}` as one object: spread it before its own `subject` or `link` if it sets its own, or the responder's wins (FAA-187 (27)).
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md
CHANGED
|
@@ -37,7 +37,7 @@ const answer = await request('GET', 'https://api.example/v1/items', { headers, d
|
|
|
37
37
|
| The body is read to one byte past `max_bytes`; over it is an error | 8 MiB | a runaway answer never fills memory |
|
|
38
38
|
| Redirects are answered, never followed | | a redirect must not carry the `Authorization` header to another host |
|
|
39
39
|
| Plain `http://` is refused unless the host is loopback or `insecure=True`; credentials in a URL are refused | | a token never crosses a network in clear, nor sits in a URL |
|
|
40
|
-
| Plain `http://` to a loopback host goes on a connection whose server end is this OS account's process, told from the socket table before a byte is sent and never through a proxy from the environment (`fabric_peer_owner`); another account's listener, root's, or one whose owner cannot be told is kind `foreign-user`, nothing sent, never retried (on a restricted macOS table its reason says `table-restricted
|
|
40
|
+
| Plain `http://` to a loopback host goes on a connection whose server end is this OS account's process, told from the socket table before a byte is sent and never through a proxy from the environment (`fabric_peer_owner`); another account's listener, root's, or one whose owner cannot be told is kind `foreign-user`, nothing sent, never retried (on a restricted macOS table its reason says `table-restricted`, which no retry clears, unless this account's own processes hold the accepted socket, DEC-0155 RT-2: protocol.md, FAA-272, FAA-292). The Node twin sends such a request through `node:http` on a checked connection, not `fetch` | | DEC-0154 HA-2: loopback is one per computer, not one per OS account |
|
|
41
41
|
| Header names must be tokens; a value with a line break or NUL is refused | | header injection |
|
|
42
42
|
| A JSON body is written as `JSON.stringify` writes it: a lone surrogate escaped, NaN and Infinity refused | | both twins send the same bytes |
|
|
43
43
|
| Whatever the transport throws is a connection error, in both languages; a timeout is a timeout | | |
|
|
@@ -85,7 +85,7 @@ const answer = await client.chat(messages, { model, fallback, maxTokens: 800 });
|
|
|
85
85
|
| The fallback | required, of another vendor (the part before `/` differs) |
|
|
86
86
|
| 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 |
|
|
87
87
|
| 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 |
|
|
88
|
-
| 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
|
|
88
|
+
| 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); 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`). One rule reads the base as both twins send it, before `check_url` (`base_url_problem`, `baseUrlProblem`, FAA-187 (18)): a backslash anywhere, a path holding anything but URL path characters and `%XX` escapes (a space, a control, a tab or line break inside, a character outside ASCII) or a dot segment, escaped or not, is kind `config` in both, where Python refused some and Node sent them percent-encoded or rewritten. A host outside ASCII is kept as urllib dials it, in IDNA, so attribution reads the host the call reaches in both twins (FAA-187 (19)) |
|
|
89
89
|
| Attribution | off unless an `attribution` setting is given (FAC-SEM-098, `fabric_usage.attribution_headers`) |
|
|
90
90
|
| 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 |
|
|
91
91
|
|
|
@@ -44,10 +44,12 @@ response = server.handle(json_rpc_message) # POST /mcp body in, JSON re
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
- A handler returns the value for `structuredContent`; for a job it returns
|
|
47
|
-
`ctx.start_job()`, and `on_job_started(job_id, ctx)` does or schedules the work.
|
|
47
|
+
`ctx.start_job()`, and `on_job_started(job_id, ctx)` does or schedules the work. An `InteropError` it raises (a
|
|
48
|
+
refused `approval_step`) fails the job just created with `tool-error` and answers the call as one (FAA-196 (28)).
|
|
48
49
|
- `jobs.request_input(job_id, {"title_choice": fi.choice_request(...)}, "Two titles are ready.")`
|
|
49
50
|
stops for a person. Fabric answers through `fabric.job.get` with `inputResponses`; the
|
|
50
|
-
server calls `on_input(job_id, answers, ctx)` with the answers that matched
|
|
51
|
+
server calls `on_input(job_id, answers, ctx)` with the answers that matched; an `InteropError` it raises is answered
|
|
52
|
+
as a `tool-error`, as a handler's is (FAA-196 (27)). `steps=` is a JSON object or none (FAA-196 (26)).
|
|
51
53
|
- `jobs.complete(job_id, fi.result_envelope(outcome=..., producer=..., ...))`,
|
|
52
54
|
`jobs.fail(job_id, code, message, data=None)`, `jobs.cancel(job_id)`; a limit or an unseen effect has its own calls
|
|
53
55
|
([Limits and uncertain effects](#limits-and-uncertain-effects)). A terminal job never changes
|
|
@@ -335,6 +337,16 @@ server.add_tool(approve_tool, approve_reply, action=reply_action, target="reply_
|
|
|
335
337
|
`authorize` accepted a delegation token, the token's caller must be the agent before it in the chain
|
|
336
338
|
(`mandate-not-for-this-call` otherwise). A T2 caller's mandate changes nothing (MD-5): it still gets
|
|
337
339
|
`approval-required`.
|
|
340
|
+
- **A per-agent credential (FAA-135).** A service that authenticates its callers with its own per-agent credentials
|
|
341
|
+
(`surfaces.mcp.auth: own`) names the caller's agent id with `McpToolServer(identify_agent=)`, a function of
|
|
342
|
+
`(headers, tier)` read when no delegation token named a caller: that agent must then be the one before `agent_id` in
|
|
343
|
+
the chain, at the call, at a job's approval step and at an answer. `None` binds nothing, as before; an id the
|
|
344
|
+
registry refuses, an answer that is not a string, or a hook that raises binds `UNREADABLE_AGENT`, which no chain
|
|
345
|
+
holds, so every mandate is refused `mandate-not-for-this-call` (fail closed). `identify` (the identity recorded) and
|
|
346
|
+
`identify_agent` (the agent id a chain is bound with) are separate hooks: an identity is not always an agent id.
|
|
347
|
+
`identified_agent(value)` / `identifiedAgent(value)` reads a hook's answer that way; Node has no server class, so a
|
|
348
|
+
Node service passes `callerAgent: identifiedAgent(<its hook's answer>)` (or `UNREADABLE_AGENT` when its hook throws)
|
|
349
|
+
to `actionGate` and `jobStep` itself. Never a name the call carries.
|
|
338
350
|
- A covered call is the operator's act: `ctx.record` is `{by: "operator", via: "mandate", task, chain, caller}` and
|
|
339
351
|
`ctx.mandate` the mandate it took (its `instruction` stays in the audit record, never in events or a page). Outside
|
|
340
352
|
it the answer is `operator-decision-required` with `data.detail` `outside-mandate: <action|capability|effect|spend|project>`
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -59,8 +59,10 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
59
59
|
| L0 | `fabric_time` | strict RFC 3339, UTC days, formatting, day windows, the clock ([dates](#dates-and-times)) |
|
|
60
60
|
| L0 | `fabric_identifiers` | identifier equality by normal form (DEC-0076) |
|
|
61
61
|
| L0 | `fabric_secrets` | the contract's one credential detector (DEC-0044) |
|
|
62
|
-
| L0 | `
|
|
62
|
+
| L0 | `fabric_socket_proof` | this OS account's own TCP sockets, read from its own processes, and the two proofs they give on a restricted macOS table (DEC-0155 RT-2, FAA-292; FAA-272): `accepted` (the server end of a connection held by one of this account's processes) and `own_listener` (its listener on exactly 127.0.0.1); Python reads them through libproc (Project Observatory's `peer_owner` 1.3.1 reader, calibrated on a pair of its own) and lsof when libproc is unavailable, the Node twin through `lsof -nP -a -u <uid> -iTCP:<port> -sTCP:<state> -F pun`; `fabric_peer_owner` asks it when it sits beside it |
|
|
63
|
+
| L0 | `fabric_peer_owner` | which OS account owns each end of a loopback connection, from the computer's socket table (DEC-0154 HA-2, FAA-226): `listener`, `server_end`, `peer`, `CheckedHTTPConnection`; the Python file is Project Observatory's `peer_owner.py`, vendored from commit `f86f0622` (1.1.0) and read against `2180235c` (1.2.0, whose Linux rule it carries) and `a69c6fee` (1.3.1), with the readings its header names (`1.3.1+kit.7`), the Node twin reads netstat, `/proc` and the owner SID where Python reads `sysctl`, `/proc` and `GetExtendedTcpTable`; on a restricted macOS table (FAA-267) `fabric_socket_proof`, when beside it, proves a server end or listener this account's processes hold (RT-2), and after it a caller may vouch with a pid it started, proved by `lsof -p` (RT-3) |
|
|
63
64
|
| L0 | `fabric_tailnet` | who calls an agent behind `tailscale serve` and what that person may open (FAA-297): `Tailscale-User-Login` only on the agent's own serve listener from a loopback peer, groups from a generated `tailnet-perimeter/1` file, a personal SHA-256-stored key for MCP no wider than the groups; `authorize`, `load_perimeter`, `load_keys`, `hash_key`, refusals 401/403 with a code ([surfaces-and-auth.md](surfaces-and-auth.md#behind-tailscale-serve)) |
|
|
65
|
+
| L0 | `fabric_tailnet_identity` | a person's identity at every door (dashboard, MCP, the service's own) of a service published through `tailscale serve` (FAA-324): `Tailscale-User-Login` believed only when the service listens on a loopback address and the peer is loopback, groups and per-door rules from the organization's `tailnet-groups/1` file (schema `tailnet-groups.schema.json`, shipped beside it as data), read again on change and closing every door while unreadable or invalid; `TailnetIdentity`, `decide`, `load_groups`, `send_refusal`, every refusal 403 with a code and a sentence ([surfaces-and-auth.md](surfaces-and-auth.md#a-groups-file-at-every-door)) |
|
|
64
66
|
| L1 | `fabric_crypto` | Ed25519 signing (a Montgomery ladder that does not branch on the key) and verification, P-256 verification, base64url, sha256 |
|
|
65
67
|
| L1 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
66
68
|
| L1 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
|
|
@@ -87,6 +89,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
87
89
|
| L3 | `fabric_auth` | the token, the network guard, login codes |
|
|
88
90
|
| L3 | `fabric_events` | events, paging, the JSONL event log (DEC-0029) |
|
|
89
91
|
| L3 | `fabric_logs` | rotating logs, the stop signal and the drain (LC-12) |
|
|
92
|
+
| L3 | `fabric_ops_digest` | the operations digests: `ops.digest.hourly@1` and `ops.digest.daily@1` folded from the service's own counts, FAC-SEM-166 and FAC-SEM-167, the OD-9 declaration (DEC-0153, FAA-206); its reference is [ops-digest.md](ops-digest.md) |
|
|
90
93
|
| L3 | `fabric_supervisors` | launchd (a LaunchAgent, or a LaunchDaemon in the system domain, FAA-278), systemd user units, Task Scheduler (DEC-0032, LC-14) |
|
|
91
94
|
| L3 | `fabric_service` | the facade: every name 0.39.1 exported, re-exported from the modules above |
|
|
92
95
|
| L3 | `fabric_profiles` | runner and model profiles ([profiles](profiles.md)) |
|
|
@@ -102,6 +105,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
102
105
|
| L4 | `fabric_board` | the facade of the board connector ([board](board.md)) |
|
|
103
106
|
| L4 | `fabric_http` | outbound HTTP: deadline, bounded retries, `Retry-After`, size cap ([connectors](connectors.md)) |
|
|
104
107
|
| L4 | `fabric_openrouter` | OpenRouter chat: own key, spend ceiling, receipts, fallback ([connectors](connectors.md)) |
|
|
108
|
+
| L4 | `fabric_a2a_server` | A2A 1.0 serving and calling: the Agent Card from capabilities, the JSON-RPC binding over the interop job store, a bounded client with the operator's origin allowlist (FAA-10); its reference is [a2a.md](a2a.md) |
|
|
105
109
|
|
|
106
110
|
Beside the kit, not in `kit.json`: the probe (`check_service.py`, `check_service_rules.py`,
|
|
107
111
|
`check_service_channel_agent.py`, `check_service_channel_agent_schema.py` and the
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -106,11 +106,14 @@ systemd and Task Scheduler do not poll yet, FAA-253).
|
|
|
106
106
|
then poll the well-known document until it answers with this `id.instance` on a listener of this OS account
|
|
107
107
|
(DEC-0154 HA-2: another account's listener, or one whose owner cannot be told, is `foreign-user` and never a
|
|
108
108
|
successful install; the error names it). Where the socket table hides other processes (macOS 27.2 under an
|
|
109
|
-
ad-hoc-signed interpreter), the
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
ad-hoc-signed interpreter), the job's accepted socket in one of this account's processes proves it first (DEC-0155
|
|
110
|
+
RT-2, `fabric_socket_proof`, FAA-292), then the installer vouches with its own job's pid from `launchctl print`,
|
|
111
|
+
for a listen on exactly `127.0.0.1` alone (RT-3; a wildcard bind is never vouched for since FAA-312); a listener
|
|
112
|
+
neither proves reads `table-restricted` (beside HA-2's `foreign-user`, which the pinned contract's definition
|
|
113
|
+
covers) and the error names the way round it (FAA-267). The vouch takes the job's own pid, so a `ProgramArguments`
|
|
114
|
+
wrapper that starts the server as a child (`sh -c` without `exec`, `uv run`, `npm start`) cannot vouch for it,
|
|
115
|
+
though RT-2 still proves its child's accepted socket: on such a host start the server with `exec`, or run the
|
|
116
|
+
installer from Apple's `/usr/bin/python3`.
|
|
114
117
|
A LaunchDaemon reads its answer differently: *The system domain*, below.
|
|
115
118
|
4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
|
|
116
119
|
again, then `prune_releases(releases_dir, current=<new release>)`: the running release
|