@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.84.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 +169 -3
- package/README.md +7 -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_identifiers.py +1 -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 +38 -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 +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -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 +28 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
- 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 +13 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
- 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 +275 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
- 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 +17 -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 +6 -5
- 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 +4 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -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
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
2
|
+
// The serving side and a bounded client of A2A 1.0 (FAA-10, DEC-0122): an Agent Card built from the service's
|
|
3
|
+
// capabilities (agentCard, checked by agentCardFindings before it is served), the JSON-RPC binding (A2ABinding:
|
|
4
|
+
// SendMessage, SendStreamingMessage, GetTask, CancelTask) mapped onto the interop job store (a task is a job, a choice
|
|
5
|
+
// stop is TASK_STATE_INPUT_REQUIRED, the result envelope an artifact), and A2AClient (discover, call, get, cancel)
|
|
6
|
+
// that reaches a peer only on an origin the operator allowed, its credential read from the environment use_secret.py
|
|
7
|
+
// run fills. The interface never takes the descriptor's token (DEC-0122 item 3).
|
|
8
|
+
// Part of the Node service kit (layer L4); the twin of fabric_a2a_server.py. Node 20+, no dependency.
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import { A2A_PROTOCOL_VERSION, A2A_SURFACE_PATH, agentCardFindings } from './fabric-a2a.mjs';
|
|
11
|
+
import { ServiceError } from './fabric-base.mjs';
|
|
12
|
+
import { InteropError, UnknownJob } from './fabric-interop-rules.mjs';
|
|
13
|
+
import { request as httpRequest } from './fabric-http.mjs';
|
|
14
|
+
|
|
15
|
+
export const JSONRPC_BINDING = 'JSONRPC';
|
|
16
|
+
export const PARSE_ERROR = -32700;
|
|
17
|
+
export const INVALID_REQUEST = -32600;
|
|
18
|
+
export const METHOD_NOT_FOUND = -32601;
|
|
19
|
+
export const INVALID_PARAMS = -32602;
|
|
20
|
+
export const INTERNAL_ERROR = -32603;
|
|
21
|
+
export const TASK_NOT_FOUND = -32001;
|
|
22
|
+
export const TASK_NOT_CANCELABLE = -32002;
|
|
23
|
+
export const UNSUPPORTED_OPERATION = -32004;
|
|
24
|
+
export const CONTENT_TYPE_NOT_SUPPORTED = -32005;
|
|
25
|
+
export const VERSION_NOT_SUPPORTED = -32009;
|
|
26
|
+
export const TASK_STATES = Object.freeze({ working: 'TASK_STATE_WORKING', input_required: 'TASK_STATE_INPUT_REQUIRED', completed: 'TASK_STATE_COMPLETED', failed: 'TASK_STATE_FAILED', cancelled: 'TASK_STATE_CANCELED' });
|
|
27
|
+
export const TERMINAL_STATES = Object.freeze(['TASK_STATE_COMPLETED', 'TASK_STATE_FAILED', 'TASK_STATE_CANCELED', 'TASK_STATE_REJECTED']);
|
|
28
|
+
export const MAX_BODY = 1024 * 1024;
|
|
29
|
+
const SKILL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
30
|
+
const STOPS = [...TERMINAL_STATES, 'TASK_STATE_INPUT_REQUIRED'];
|
|
31
|
+
const hex = (n) => crypto.randomBytes(n).toString('hex');
|
|
32
|
+
|
|
33
|
+
/** A refused A2A call or card; the message never carries a credential. */
|
|
34
|
+
export class A2AError extends ServiceError {}
|
|
35
|
+
|
|
36
|
+
export function skillFromCapability(capability) {
|
|
37
|
+
const name = capability?.name;
|
|
38
|
+
if (typeof name !== 'string' || !SKILL_ID.test(name)) throw new A2AError("a skill's id is the capability's name: letters, digits, '.', '_' or '-', at most 64");
|
|
39
|
+
const skill = { id: name, name: capability.title || name, description: capability.description || name, tags: [...(capability.tags || [name.split('.')[0]])] };
|
|
40
|
+
for (const key of ['examples', 'inputModes', 'outputModes']) if (capability[key]?.length) skill[key] = [...capability[key]];
|
|
41
|
+
return skill;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function agentCard({ name, description, version, url, capabilities, securitySchemes, streaming = false, extensions, remote = false, descriptorHeader = 'Authorization' }) {
|
|
45
|
+
const match = /^(https?:\/\/[^/?#]+)/.exec(url ?? '');
|
|
46
|
+
const card = {
|
|
47
|
+
name, description, version,
|
|
48
|
+
supportedInterfaces: [{ url, protocolBinding: JSONRPC_BINDING, protocolVersion: A2A_PROTOCOL_VERSION }],
|
|
49
|
+
capabilities: { streaming: Boolean(streaming), pushNotifications: false },
|
|
50
|
+
defaultInputModes: ['text/plain', 'application/json'], defaultOutputModes: ['text/plain', 'application/json'],
|
|
51
|
+
skills: capabilities.map(skillFromCapability),
|
|
52
|
+
};
|
|
53
|
+
if (extensions?.length) card.capabilities.extensions = extensions.map((e) => ({ ...e, required: false }));
|
|
54
|
+
if (securitySchemes) card.securitySchemes = securitySchemes;
|
|
55
|
+
const findings = agentCardFindings(card, { origin: match ? match[1] : undefined, remote, descriptorHeader });
|
|
56
|
+
if (findings.length) throw new A2AError(`the card is not served: ${findings.map((f) => f.message).join('; ')}`);
|
|
57
|
+
return card;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const textOf = (message) => (Array.isArray(message?.parts) ? message.parts : []).filter((p) => typeof p?.text === 'string').map((p) => p.text).join('\n');
|
|
61
|
+
export const dataOf = (message) => (Array.isArray(message?.parts) ? message.parts : []).filter((p) => p && typeof p === 'object' && 'data' in p).map((p) => p.data);
|
|
62
|
+
|
|
63
|
+
export function agentMessage(text, { taskId, contextId, data } = {}) {
|
|
64
|
+
const parts = [{ text }];
|
|
65
|
+
if (data !== undefined) parts.push({ data });
|
|
66
|
+
const message = { role: 'ROLE_AGENT', parts, messageId: `msg-${hex(8)}` };
|
|
67
|
+
if (taskId) message.taskId = taskId;
|
|
68
|
+
if (contextId) message.contextId = contextId;
|
|
69
|
+
return message;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What a message handler gets: the message, its caller and start(skill), which makes the job that is the task. */
|
|
73
|
+
export class TaskStart {
|
|
74
|
+
constructor(binding, message, caller, contextId) { Object.assign(this, { binding, message, caller, contextId, taskId: null }); }
|
|
75
|
+
start(skill) {
|
|
76
|
+
if (!this.binding.skills.has(skill)) throw new A2AError(`no skill ${skill} on this card`);
|
|
77
|
+
const handle = this.binding.jobs.create(`a2a.${skill}`, { a2a: { message: this.message, contextId: this.contextId, caller: this.caller } });
|
|
78
|
+
this.taskId = handle.job.id;
|
|
79
|
+
return this.taskId;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class NotCancelable extends Error {}
|
|
84
|
+
class BadParams extends Error {}
|
|
85
|
+
|
|
86
|
+
const result = (id, value) => ({ jsonrpc: '2.0', id, result: value });
|
|
87
|
+
const error = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
88
|
+
const event = (id, value) => `data: ${JSON.stringify(result(id, value))}\n\n`;
|
|
89
|
+
|
|
90
|
+
function textAnswer(requests, text) {
|
|
91
|
+
const keys = Object.keys(requests ?? {});
|
|
92
|
+
if (keys.length !== 1 || !text) return null;
|
|
93
|
+
const props = Object.keys(requests[keys[0]]?.params?.requestedSchema?.properties ?? {});
|
|
94
|
+
return props.length === 1 ? { [keys[0]]: { action: 'accept', content: { [props[0]]: text } } } : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The A2A Task an interop job reads as. */
|
|
98
|
+
export function taskOf(job, contextId) {
|
|
99
|
+
const status = { state: TASK_STATES[job.status] ?? 'TASK_STATE_UNSPECIFIED', timestamp: job.updatedAt ?? '' };
|
|
100
|
+
if (job.status === 'input_required') {
|
|
101
|
+
const questions = Object.values(job.inputRequests ?? {}).map((r) => r?.params?.message).filter((q) => typeof q === 'string');
|
|
102
|
+
status.message = agentMessage(job.statusMessage || questions.join('; ') || 'an answer is needed', { taskId: job.id, contextId, data: { inputRequests: job.inputRequests } });
|
|
103
|
+
} else if (job.status === 'failed') {
|
|
104
|
+
status.message = agentMessage(String(job.error?.message || 'the task failed'), { taskId: job.id, contextId, data: { code: job.error?.code } });
|
|
105
|
+
} else if (job.statusMessage) {
|
|
106
|
+
status.message = agentMessage(job.statusMessage, { taskId: job.id, contextId });
|
|
107
|
+
}
|
|
108
|
+
const task = { id: job.id, contextId, status };
|
|
109
|
+
if (job.status === 'completed') task.artifacts = [{ artifactId: 'result', name: 'result', parts: [{ data: job.result }] }];
|
|
110
|
+
return task;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The JSON-RPC binding over the interop job store; see fabric_a2a_server.A2ABinding. */
|
|
114
|
+
export class A2ABinding {
|
|
115
|
+
constructor(jobs, handler, { skills, authorize, clock = () => Date.now() / 1000 }) {
|
|
116
|
+
Object.assign(this, { jobs, handler, skills: new Set(skills), authorize, clock });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
task(jobId, caller) {
|
|
120
|
+
const a2a = this.jobs.load(jobId).private?.input?.a2a;
|
|
121
|
+
if (!a2a || typeof a2a !== 'object' || (a2a.caller ?? null) !== (caller ?? null)) throw new UnknownJob(jobId);
|
|
122
|
+
return taskOf(this.jobs.get(jobId), a2a.contextId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
handle(body, headers = {}) {
|
|
126
|
+
const lowered = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
127
|
+
const caller = this.authorize(lowered);
|
|
128
|
+
if (caller === null || caller === undefined) return [401, null];
|
|
129
|
+
if (body instanceof Uint8Array) {
|
|
130
|
+
if (body.length > MAX_BODY) return [413, null];
|
|
131
|
+
try { body = JSON.parse(Buffer.from(body).toString('utf8')); } catch { return [200, error(null, PARSE_ERROR, 'the body is not JSON')]; }
|
|
132
|
+
}
|
|
133
|
+
if (!body || typeof body !== 'object' || Array.isArray(body) || body.jsonrpc !== '2.0' || typeof body.method !== 'string') {
|
|
134
|
+
return [200, error(body && typeof body === 'object' && !Array.isArray(body) ? body.id : null, INVALID_REQUEST, 'not a JSON-RPC 2.0 request')];
|
|
135
|
+
}
|
|
136
|
+
const rid = body.id;
|
|
137
|
+
const version = lowered['a2a-version'];
|
|
138
|
+
if (version !== undefined && String(version).trim() !== A2A_PROTOCOL_VERSION) return [200, error(rid, VERSION_NOT_SUPPORTED, `this agent speaks A2A ${A2A_PROTOCOL_VERSION}`)];
|
|
139
|
+
const params = body.params;
|
|
140
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) return [200, error(rid, INVALID_PARAMS, 'params is an object')];
|
|
141
|
+
try {
|
|
142
|
+
if (body.method === 'SendStreamingMessage') return [200, error(rid, UNSUPPORTED_OPERATION, 'streaming is served by stream() as text/event-stream')];
|
|
143
|
+
if (body.method === 'SendMessage') return [200, result(rid, this.send(params, caller))];
|
|
144
|
+
if (body.method === 'GetTask') return [200, result(rid, this.task(String(params.id), caller))];
|
|
145
|
+
if (body.method === 'CancelTask') return [200, result(rid, this.cancel(String(params.id), caller))];
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (e instanceof UnknownJob) return [200, error(rid, TASK_NOT_FOUND, 'no such task')];
|
|
148
|
+
if (e instanceof NotCancelable) return [200, error(rid, TASK_NOT_CANCELABLE, 'the task has ended')];
|
|
149
|
+
if (e instanceof BadParams) return [200, error(rid, INVALID_PARAMS, e.message)];
|
|
150
|
+
if (e instanceof A2AError || e instanceof InteropError) return [200, error(rid, INTERNAL_ERROR, `the agent could not take the message: ${e.message}`)];
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
return [200, error(rid, METHOD_NOT_FOUND, `no method ${body.method.slice(0, 64)} here`)];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
send(params, caller) {
|
|
157
|
+
const message = params.message;
|
|
158
|
+
if (!message || typeof message !== 'object' || !Array.isArray(message.parts) || !message.parts.length) throw new BadParams('params.message is a message with parts');
|
|
159
|
+
if (typeof message.taskId === 'string' && message.taskId) return { task: this.continueTask(message.taskId, message, caller) };
|
|
160
|
+
const contextId = typeof message.contextId === 'string' ? message.contextId : `ctx-${hex(8)}`;
|
|
161
|
+
const start = new TaskStart(this, message, caller, contextId);
|
|
162
|
+
const reply = this.handler(start);
|
|
163
|
+
if (start.taskId !== null) return { task: this.task(start.taskId, caller) };
|
|
164
|
+
if (!reply || typeof reply !== 'object') throw new A2AError('the handler neither replied nor started a task');
|
|
165
|
+
return { message: { ...reply, contextId } };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
continueTask(taskId, message, caller) {
|
|
169
|
+
const task = this.task(taskId, caller);
|
|
170
|
+
if (task.status.state !== 'TASK_STATE_INPUT_REQUIRED') throw new BadParams('the task waits for no answer');
|
|
171
|
+
const job = this.jobs.get(taskId);
|
|
172
|
+
let responses = dataOf(message).find((d) => d && typeof d === 'object' && d.inputResponses && typeof d.inputResponses === 'object' && !Array.isArray(d.inputResponses))?.inputResponses ?? null;
|
|
173
|
+
if (responses === null) responses = textAnswer(job.inputRequests ?? {}, textOf(message));
|
|
174
|
+
if (!responses || !Object.keys(this.jobs.answer(taskId, responses) ?? {}).length) throw new BadParams("the answer names none of the task's questions");
|
|
175
|
+
return this.task(taskId, caller);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
cancel(taskId, caller) {
|
|
179
|
+
const task = this.task(taskId, caller);
|
|
180
|
+
if (TERMINAL_STATES.includes(task.status.state)) throw new NotCancelable();
|
|
181
|
+
this.jobs.cancel(taskId);
|
|
182
|
+
return this.task(taskId, caller);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async *stream(params, caller, { rid = null, pollS = 1, deadlineS = 300, sleep = (s) => new Promise((r) => setTimeout(r, s * 1000)) } = {}) {
|
|
186
|
+
const first = this.send(params, caller);
|
|
187
|
+
yield event(rid, first);
|
|
188
|
+
if (!first.task) return;
|
|
189
|
+
let task = first.task;
|
|
190
|
+
const end = this.clock() + deadlineS;
|
|
191
|
+
while (!STOPS.includes(task.status.state) && this.clock() < end) {
|
|
192
|
+
await sleep(pollS);
|
|
193
|
+
const now = this.task(task.id, caller);
|
|
194
|
+
if (now.status.state !== task.status.state || JSON.stringify(now.artifacts) !== JSON.stringify(task.artifacts)) {
|
|
195
|
+
yield event(rid, { statusUpdate: { taskId: now.id, contextId: now.contextId, status: now.status, final: STOPS.includes(now.status.state) } });
|
|
196
|
+
}
|
|
197
|
+
task = now;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** scheme://host[:port] of a URL, lower-cased, a default port dropped. */
|
|
203
|
+
export function originOf(url) {
|
|
204
|
+
const match = /^(https?):\/\/([^/?#]+)/i.exec(url ?? '');
|
|
205
|
+
if (!match || match[2].includes('@')) throw new A2AError("a peer's address is an http(s) origin with no userinfo");
|
|
206
|
+
const scheme = match[1].toLowerCase();
|
|
207
|
+
let host = match[2].toLowerCase();
|
|
208
|
+
const port = host.split(':').pop();
|
|
209
|
+
if (((scheme === 'https' && port === '443') || (scheme === 'http' && port === '80')) && host.includes(':') && !host.endsWith(']')) host = host.slice(0, host.lastIndexOf(':'));
|
|
210
|
+
return `${scheme}://${host}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A bounded A2A 1.0 client; see fabric_a2a_server.A2AClient. */
|
|
214
|
+
export class A2AClient {
|
|
215
|
+
constructor(allowOrigins, { credentialEnv, header = 'Authorization', scheme = 'Bearer', deadlineS = 30, maxBytes = MAX_BODY, transport, insecure = false } = {}) {
|
|
216
|
+
this.allowed = new Set(allowOrigins.map(originOf));
|
|
217
|
+
if (!this.allowed.size) throw new A2AError("a client needs the operator's list of peer origins");
|
|
218
|
+
Object.assign(this, { credentialEnv, header, scheme, deadlineS, maxBytes, transport, insecure, ids: 0 });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
check(url) { if (!this.allowed.has(originOf(url))) throw new A2AError(`${originOf(url)} is not on the operator's list of A2A peers`); }
|
|
222
|
+
|
|
223
|
+
headers() {
|
|
224
|
+
const headers = { 'A2A-Version': A2A_PROTOCOL_VERSION, Accept: 'application/json' };
|
|
225
|
+
if (this.credentialEnv) {
|
|
226
|
+
const value = process.env[this.credentialEnv];
|
|
227
|
+
if (!value) throw new A2AError(`${this.credentialEnv} is empty: run the caller under use_secret.py run, which places the key`);
|
|
228
|
+
headers[this.header] = this.scheme ? `${this.scheme} ${value}` : value;
|
|
229
|
+
}
|
|
230
|
+
return headers;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
options(extra) { return { deadlineS: this.deadlineS, maxBytes: this.maxBytes, insecure: this.insecure, ...(this.transport ? { transport: this.transport } : {}), ...extra }; }
|
|
234
|
+
|
|
235
|
+
async discover(origin) {
|
|
236
|
+
const url = originOf(origin) + A2A_SURFACE_PATH;
|
|
237
|
+
this.check(url);
|
|
238
|
+
const response = await httpRequest('GET', url, this.options({ headers: { Accept: 'application/json' } }));
|
|
239
|
+
if (response.status !== 200) throw new A2AError(`the peer answered HTTP ${response.status} for its card`);
|
|
240
|
+
let card;
|
|
241
|
+
try { card = response.json(); } catch { throw new A2AError("the peer's card is not JSON"); }
|
|
242
|
+
if (!card || typeof card !== 'object' || Array.isArray(card)) throw new A2AError("the peer's card is not a JSON object");
|
|
243
|
+
for (const i of card.supportedInterfaces ?? []) {
|
|
244
|
+
if (i && i.protocolBinding === JSONRPC_BINDING && i.protocolVersion === A2A_PROTOCOL_VERSION && typeof i.url === 'string') {
|
|
245
|
+
this.check(i.url);
|
|
246
|
+
return { card, url: i.url };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw new A2AError(`the peer's card names no A2A ${A2A_PROTOCOL_VERSION} JSON-RPC interface`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async rpc(url, method, params) {
|
|
253
|
+
this.check(url);
|
|
254
|
+
this.ids += 1;
|
|
255
|
+
const response = await httpRequest('POST', url, this.options({ headers: this.headers(), body: { jsonrpc: '2.0', id: this.ids, method, params }, retries: 0 }));
|
|
256
|
+
if (response.status !== 200) throw new A2AError(`the peer answered HTTP ${response.status} to ${method}`);
|
|
257
|
+
let answer = null;
|
|
258
|
+
try { answer = response.json(); } catch { /* answered below */ }
|
|
259
|
+
if (!answer || typeof answer !== 'object' || Array.isArray(answer)) throw new A2AError(`the peer's answer to ${method} is not a JSON-RPC response`);
|
|
260
|
+
if ('error' in answer) throw new A2AError(`the peer refused ${method}: ${answer.error?.code} ${String(answer.error?.message).slice(0, 200)}`);
|
|
261
|
+
return answer.result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
call(url, text, { data, taskId, contextId } = {}) {
|
|
265
|
+
const parts = [{ text }];
|
|
266
|
+
if (data !== undefined) parts.push({ data });
|
|
267
|
+
const message = { role: 'ROLE_USER', parts, messageId: `msg-${hex(8)}` };
|
|
268
|
+
if (taskId) message.taskId = taskId;
|
|
269
|
+
if (contextId) message.contextId = contextId;
|
|
270
|
+
return this.rpc(url, 'SendMessage', { message });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
get(url, taskId) { return this.rpc(url, 'GetTask', { id: taskId }); }
|
|
274
|
+
cancel(url, taskId) { return this.rpc(url, 'CancelTask', { id: taskId }); }
|
|
275
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The reading side of an A2A Agent Card (DEC-0122): the well-known document's surfaces.a2a member, read as the contract's
|
|
2
3
|
// schema reads it (a2aSurfaceFindings, a2aView; a2aSurface builds one), and what a public card must not carry
|
|
3
4
|
// (agentCardFindings): no member about the running process, no secret, no securitySchemes entry naming the descriptor's
|
|
4
5
|
// token header, every Fabric extension required: false (or absent, which A2A reads as false), and interface URLs where the placement puts them. Serving a
|
|
5
|
-
// card, the A2A binding and a client are
|
|
6
|
+
// card, the A2A binding and a client are in fabric-a2a-server.mjs (FAA-10).
|
|
6
7
|
// Part of the Node service kit (layer L3); the twin of fabric_a2a.py. Node 20+, no dependency.
|
|
7
8
|
import { ServiceError } from './fabric-base.mjs';
|
|
8
9
|
import { isObject } from './fabric-jsvalue.mjs';
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// Every action a person has, its agent tool and who decides it: fabric-actions/0.1, the extension keyed
|
|
2
3
|
// extensions/actions/0.1 (DEC-0121, FAC-SEM-142). Part of the Node service kit (layer L3); the twin of fabric_actions.py.
|
|
3
4
|
//
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// Who may call a service: the descriptor token, the network guard, the operator's one-time login and session, remote placement (DEC-0019), the feed client's token header.
|
|
2
3
|
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_auth.py. Node 20+, no dependency.
|
|
3
4
|
import crypto from 'node:crypto';
|
|
@@ -7,7 +8,7 @@ import { PROTOCOL, ServiceError, nowIso } from './fabric-base.mjs';
|
|
|
7
8
|
import { tokenMatches, windowsTokenProblem } from './fabric-platform.mjs';
|
|
8
9
|
import { atomicWrite, ensurePrivateDir, servicesDir } from './fabric-paths.mjs';
|
|
9
10
|
import { isPlainObject } from './fabric-usage.mjs';
|
|
10
|
-
import { descriptorPath, namesRepository, sourceRefusal, validateDescriptor, writeDescriptor } from './fabric-descriptor.mjs';
|
|
11
|
+
import { descriptorPath, namesRepository, sourceRefusal, validateDescriptor, warnNoSource, writeDescriptor } from './fabric-descriptor.mjs';
|
|
11
12
|
|
|
12
13
|
export const SESSION_COOKIE = 'fabric_session';
|
|
13
14
|
|
|
@@ -286,9 +287,11 @@ export function registerRemote({ id, instance = 'default', name, summary, origin
|
|
|
286
287
|
if (problems.length) throw new ServiceError(`Descriptor is invalid: ${problems.join('; ')}.`);
|
|
287
288
|
const refusal = sourceRefusal(d, dir); // before the token is written: a refused install leaves nothing (FAA-198)
|
|
288
289
|
if (refusal) throw new ServiceError(refusal);
|
|
290
|
+
const warned = !namesRepository(d);
|
|
291
|
+
if (warned) warnNoSource(d); // before the token: a warning that throws leaves both files as they were (FAA-200 (1))
|
|
289
292
|
ensurePrivateDir(path.dirname(tokenFile));
|
|
290
293
|
atomicWrite(tokenFile, String(token).trim(), 0o600);
|
|
291
|
-
return writeDescriptor(d, dir);
|
|
294
|
+
return writeDescriptor(d, dir, { warned });
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
export function cookieValue(header, name = SESSION_COOKIE) {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// Settings backups (DEC-0025, FAC-SEM-035): canonical form, digest, write, list, check, restore.
|
|
2
3
|
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_backups.py. Node 20+, no dependency.
|
|
3
4
|
import crypto from 'node:crypto';
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// What every module of the Node service kit shares: the protocol name, ServiceError, ids, nowIso and expand.
|
|
2
3
|
// Part of the Node service kit (FAA-65, layer L1); the twin of fabric_base.py. Node 20+, no dependency.
|
|
3
4
|
import os from 'node:os';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// A fixture conversation for the probe (check_service.py, rule board.responder): a scripted board a responder can run against without Fabric.
|
|
2
3
|
// Part of the Node board connector (FAA-65, layer L4); the twin of fabric_board_fixture.py. Node 20+, no dependency.
|
|
3
4
|
import fs from 'node:fs';
|
|
@@ -5,6 +6,7 @@ import path from 'node:path';
|
|
|
5
6
|
import { Backoff, HubError, MCP_PROTOCOL_VERSION, normalProject, postMcp, toolAnswer } from './fabric-hub.mjs';
|
|
6
7
|
import { COMMS_PROTOCOL, RESPOND_TOOLS } from './fabric-board-shapes.mjs';
|
|
7
8
|
import { BoardSession, Responder } from './fabric-board-session.mjs';
|
|
9
|
+
import { LEVELS } from './fabric-events.mjs'; // the levels the event log takes (FAA-187 (23))
|
|
8
10
|
|
|
9
11
|
const FIXTURE_PROJECT = '3f2a6b1c-0d4e-4f5a-8b6c-7d8e9f0a1b2c';
|
|
10
12
|
|
|
@@ -28,7 +30,11 @@ function fixtureAnswers(variant) {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* One scripted conversation per path through this kit, with no Fabric; answers the problems found (empty: it passes).
|
|
35
|
+
* Its notices are recorded: one at a level the event log refuses, one without its subject, or a fenced attempt with no
|
|
36
|
+
* warning fails it (FAA-187 (23)).
|
|
37
|
+
*/
|
|
32
38
|
export async function selfCheck() {
|
|
33
39
|
const os = await import('node:os');
|
|
34
40
|
const problems = [];
|
|
@@ -45,6 +51,8 @@ export async function selfCheck() {
|
|
|
45
51
|
const answers = fixtureAnswers(variant);
|
|
46
52
|
const calls = [];
|
|
47
53
|
const handled = [];
|
|
54
|
+
const notices = []; // recorded as JsonlEventLog.append takes them: (kind, level, text, {notify, subject, ...}) (FAA-187 (23))
|
|
55
|
+
const emit = (kind, level, text, options = {}) => { notices.push([kind, level, options?.subject]); };
|
|
48
56
|
const post = async (origin, body, bearer, { method = 'POST' } = {}) => {
|
|
49
57
|
if (method === 'DELETE') return { status: 200, headers: {}, body: null };
|
|
50
58
|
if (body.method === 'initialize') return { status: 200, headers: { 'mcp-session-id': 'fixture-session' }, body: { jsonrpc: '2.0', id: body.id, result: { protocolVersion: MCP_PROTOCOL_VERSION } } };
|
|
@@ -58,9 +66,14 @@ export async function selfCheck() {
|
|
|
58
66
|
const { HubClient } = await import('./fabric-hub.mjs');
|
|
59
67
|
const responder = new Responder({ hub: new HubClient(root, { post }), projectId: FIXTURE_PROJECT, capabilities: ['fixture.render'], credentialFile: cred,
|
|
60
68
|
checkpointFile: path.join(tmp, 'checkpoint.json'), handler: (request) => { handled.push(request.body); return { state: 'completed', summary: 'Rendered.' }; },
|
|
61
|
-
admit: () => null, now: () => 0, rand: () => 0.5, sessionFactory: (grant) => new BoardSession(grant, { post }) });
|
|
69
|
+
admit: () => null, now: () => 0, rand: () => 0.5, sessionFactory: (grant) => new BoardSession(grant, { post }), emit });
|
|
62
70
|
await responder.step();
|
|
63
71
|
const tools = calls.map(([t]) => t);
|
|
72
|
+
for (const [kind, level, subject] of notices) {
|
|
73
|
+
if (!LEVELS.includes(level)) problems.push(`${variant}: notice ${kind} at level '${level}', which the event log refuses`);
|
|
74
|
+
if (!(subject && typeof subject === 'object' && subject.type && subject.id)) problems.push(`${variant}: notice ${kind} without its subject`);
|
|
75
|
+
}
|
|
76
|
+
if (variant === 'fenced' && !notices.some(([, level]) => level === 'warning')) problems.push('fenced: no warning notice reached the service\'s emit');
|
|
64
77
|
if (tools.some((t) => !RESPOND_TOOLS.has(t) && t !== 'fabric.board.join')) problems.push(`${variant}: a tool outside the respond scope was sent`);
|
|
65
78
|
if (variant === 'whole') {
|
|
66
79
|
if (tools.join(',') !== 'fabric.board.join,com.status,com.enroll,com.claim,com.get,com.accept,com.complete') problems.push(`whole: the calls were ${tools.join(', ')}`);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The board link (DEC-0078, FAA-12b): the board session over the hub, the fsynced checkpoint and a claimed request as data.
|
|
2
3
|
// Part of the Node board connector (FAA-65, layer L4), below the responder in fabric-board-session.mjs, which re-exports
|
|
3
4
|
// these names; the twin of fabric_board_link.py. Node 18+, no dependency.
|
|
@@ -6,7 +7,7 @@ import path from 'node:path';
|
|
|
6
7
|
import { HubError, MCP_PROTOCOL_VERSION, postMcp, toolAnswer } from './fabric-hub.mjs';
|
|
7
8
|
import { atomicWrite, ensurePrivateDir } from './fabric-service.mjs';
|
|
8
9
|
import { BoardError, RESPOND_TOOLS, isObj, refusalProblems } from './fabric-board-shapes.mjs';
|
|
9
|
-
import { UNREADABLE_PROJECT, mandateSpend } from './fabric-interop-rules.mjs'; // FAA-139
|
|
10
|
+
import { UNREADABLE_AGENT, UNREADABLE_PROJECT, identifiedAgent, mandateSpend } from './fabric-interop-rules.mjs'; // FAA-139, FAA-135
|
|
10
11
|
import { isAgentId, mandateCovers } from './fabric-mandate.mjs';
|
|
11
12
|
import { formatRfc3339 } from './fabric-time.mjs';
|
|
12
13
|
|
|
@@ -26,9 +27,9 @@ export class BoardSession {
|
|
|
26
27
|
return run;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
async #send(body, method = 'POST', timeout = null) {
|
|
30
|
+
async #send(body, method = 'POST', timeout = null, deadline = 0) {
|
|
30
31
|
try {
|
|
31
|
-
return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: timeout > 0 ? timeout : this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined });
|
|
32
|
+
return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: timeout > 0 ? timeout : this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined, ...(deadline > 0 ? { deadline } : {}) });
|
|
32
33
|
} catch (e) {
|
|
33
34
|
// FAA-252: another OS account's process holds the port; nothing was sent
|
|
34
35
|
if (e instanceof HubError && e.code === 'foreign-user') throw new BoardError('foreign-user', 'The hub\'s port is held by another OS account\'s process; nothing was sent.', true);
|
|
@@ -71,13 +72,13 @@ export class BoardSession {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/** Sends the DELETE (waiting `timeout` ms at most); true when Fabric answered it with a 2xx, so the slots are free now (FAA-163). A 404 (already gone) is false: a move made before it ended is read at the next com.status, so the slot records stay. */
|
|
75
|
+
/** Sends the DELETE (waiting `timeout` ms at most for the whole of it, FAA-187 (12)); true when Fabric answered it with a 2xx, so the slots are free now (FAA-163). A 404 (already gone) is false: a move made before it ended is read at the next com.status, so the slot records stay. */
|
|
75
76
|
close(timeout = null) {
|
|
76
77
|
return this.#serial(async () => {
|
|
77
78
|
if (!this.mcpSession) return false;
|
|
78
79
|
let status = 0;
|
|
79
80
|
try {
|
|
80
|
-
const answer = await this.#send({}, 'DELETE', timeout);
|
|
81
|
+
const answer = await this.#send({}, 'DELETE', timeout, timeout > 0 ? timeout : 0); // the whole DELETE (FAA-187 (12))
|
|
81
82
|
status = Number.isInteger(answer?.status) ? answer.status : 0; // an answer it cannot read is no release, as in Python
|
|
82
83
|
} catch {
|
|
83
84
|
// best effort: the lease lapses in 60 s anyway
|
|
@@ -92,7 +93,7 @@ export class BoardSession {
|
|
|
92
93
|
export class Checkpoint {
|
|
93
94
|
constructor(file) {
|
|
94
95
|
this.file = file;
|
|
95
|
-
this.slots =
|
|
96
|
+
this.slots = Object.create(null); // the responder's own record of each slot it held: {project, generation, moved?} (FAA-155); no prototype, so a capability named `constructor` reads as any other (FAA-196 (24))
|
|
96
97
|
this.entries = this.#load();
|
|
97
98
|
}
|
|
98
99
|
#load() {
|
|
@@ -111,7 +112,7 @@ export class Checkpoint {
|
|
|
111
112
|
}
|
|
112
113
|
if (!isObj(doc) || doc.version !== 1 || !isObj(doc.requests)) throw new BoardError('checkpoint', `The board checkpoint ${this.file} is not one this kit writes.`);
|
|
113
114
|
// a record that is not an object is ignored, as a file without one is
|
|
114
|
-
this.slots = isObj(doc.slots) ? Object.fromEntries(Object.entries(doc.slots).filter(([, v]) => isObj(v))) : {};
|
|
115
|
+
this.slots = Object.assign(Object.create(null), isObj(doc.slots) ? Object.fromEntries(Object.entries(doc.slots).filter(([, v]) => isObj(v))) : {});
|
|
115
116
|
return doc.requests;
|
|
116
117
|
}
|
|
117
118
|
#write(entries, slots = this.slots) {
|
|
@@ -137,7 +138,8 @@ export class Checkpoint {
|
|
|
137
138
|
}
|
|
138
139
|
/** Writes the record of a slot this responder held (null forgets it); nothing is written when it is unchanged. */
|
|
139
140
|
slot(capability, record) {
|
|
140
|
-
const
|
|
141
|
+
const slots = Object.assign(Object.create(null), this.slots);
|
|
142
|
+
delete slots[capability];
|
|
141
143
|
if (record !== null) slots[capability] = JSON.parse(JSON.stringify(record));
|
|
142
144
|
if (JSON.stringify(sortDeep(slots)) === JSON.stringify(sortDeep(this.slots))) return;
|
|
143
145
|
this.#write(this.entries, slots);
|
|
@@ -239,7 +241,7 @@ export function readMandate(request, mandateFrom, agentId) {
|
|
|
239
241
|
try {
|
|
240
242
|
value = JSON.parse(block);
|
|
241
243
|
} catch {
|
|
242
|
-
if (
|
|
244
|
+
if (/mandate(?:\/|\\\/|\\u002[fF])1/.test(block)) return { mandate: null, unread: 'mandate-unreadable' }; // the kind as written, its slash JSON-escaped too (FAA-196 (13))
|
|
243
245
|
continue;
|
|
244
246
|
}
|
|
245
247
|
if (isObj(value) && value.kind === 'mandate/1') {
|
|
@@ -263,7 +265,7 @@ export function readMandate(request, mandateFrom, agentId) {
|
|
|
263
265
|
* absent (an inherited, non-enumerable, getter or Proxy member included); the rest of the call is never touched. A
|
|
264
266
|
* getter whose value changes is answered on its one read.
|
|
265
267
|
*/
|
|
266
|
-
export function covers(mandate, call, agentId, wall, caller = null) {
|
|
268
|
+
export function covers(mandate, call, agentId, wall, caller = null, callerAgent = null) {
|
|
267
269
|
if (mandate == null || agentId == null) return null;
|
|
268
270
|
if (call !== null && typeof call === 'object' && !Array.isArray(call)) {
|
|
269
271
|
const read = {};
|
|
@@ -284,10 +286,52 @@ export function covers(mandate, call, agentId, wall, caller = null) {
|
|
|
284
286
|
} catch { // finite but outside the dates a clock can name
|
|
285
287
|
now = '';
|
|
286
288
|
}
|
|
287
|
-
const
|
|
289
|
+
const inputs = { mandate, tier: 'T1', now, self: agentId, call };
|
|
290
|
+
if (callerAgent !== null && callerAgent !== undefined) inputs.callerAgent = callerAgent; // FAA-135
|
|
291
|
+
const answer = mandateCovers(inputs);
|
|
288
292
|
return answer.recordAs ? { ok: true, recordAs: { ...answer.recordAs, caller } } : answer;
|
|
289
293
|
}
|
|
290
294
|
|
|
295
|
+
/**
|
|
296
|
+
* A responder's `links` (FAA-187 (21)): the page on the service that resolves each notify kind, such as
|
|
297
|
+
* `{'board.slot.moved': '/#board'}`, passed to a notice that notifies as its `link`. A path on the service, as makeEvent
|
|
298
|
+
* takes one; null is none. A service that gives links has an emit that takes `link`.
|
|
299
|
+
*/
|
|
300
|
+
export function noticeLinks(links) {
|
|
301
|
+
if (links === null || links === undefined) return Object.create(null);
|
|
302
|
+
if (!isObj(links) || !Object.entries(links).every(([, v]) => typeof v === 'string' && v.startsWith('/') && !v.startsWith('//'))) {
|
|
303
|
+
throw new BoardError('invalid_arguments', 'links maps an event kind to a path on this service, such as /#board.');
|
|
304
|
+
}
|
|
305
|
+
return Object.assign(Object.create(null), links);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Why a message read back with com.get is not the request a claim took (the first path refuses it, a resume reads no
|
|
310
|
+
* mandate from it, FAA-196 (1)): `digest-mismatch` (another digest than the claim's), `not-addressed` (not this
|
|
311
|
+
* request, capability and Project), else null.
|
|
312
|
+
*/
|
|
313
|
+
export function readBackProblem(message, entry, project) {
|
|
314
|
+
if (!isObj(message) || message.digest !== entry.digest) return 'digest-mismatch';
|
|
315
|
+
const req = message.request;
|
|
316
|
+
if (!isObj(req) || req.id !== entry.request || req.capability !== entry.capability || req.target !== project) return 'not-addressed';
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* FAA-135: the agent id the service's identifyAgent(sender) names for a request's sender (the board's verified
|
|
322
|
+
* `{project, principal}`), read as identifiedAgent reads a hook's answer: null or undefined is none, an agent id is that
|
|
323
|
+
* id, anything else, or a hook that throws, is UNREADABLE_AGENT, which no chain holds. Asked only when a mandate
|
|
324
|
+
* travelled (`mandate` not null).
|
|
325
|
+
*/
|
|
326
|
+
export function senderAgent(identifyAgent, sender, mandate = true) {
|
|
327
|
+
if (identifyAgent == null || mandate == null) return null;
|
|
328
|
+
try {
|
|
329
|
+
return identifiedAgent(identifyAgent(sender));
|
|
330
|
+
} catch {
|
|
331
|
+
return UNREADABLE_AGENT; // the service's own hook: fail closed
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
291
335
|
/** The responder's opt-in, checked: answers `mandateFrom` as the responder keeps it (a Set or the string). */
|
|
292
336
|
export function mandateOptions(agentId, mandateFrom) {
|
|
293
337
|
if (agentId != null && !isAgentId(agentId)) throw new BoardError('invalid_arguments', 'agentId must be the service\'s registry agent id.');
|