@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/README.md +13 -2
- package/SECURITY.md +23 -8
- package/bin/fabric-agent-adapter.js +50 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A key-delivery door template (FAA-238), the twin of door.py: the same flags, the same exit codes, the same one line
|
|
3
|
+
// on stdout. Read door.py's docstring for the rules. Node 20+, no dependency.
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
|
|
9
|
+
export const ENVELOPE = 'fabric-credential-envelope/1';
|
|
10
|
+
const MAX_ANSWER = 65536;
|
|
11
|
+
const RETRIES = 2;
|
|
12
|
+
const TRANSPORT_EXIT = 75;
|
|
13
|
+
const TIME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z$/;
|
|
14
|
+
const B64URL = /^[A-Za-z0-9_-]+$/;
|
|
15
|
+
const SIG = /^[A-Za-z0-9_-]{86}$/;
|
|
16
|
+
const KEM = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}#kem$/;
|
|
17
|
+
const SIGNER = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}(?:#sig)?$/;
|
|
18
|
+
const CONTEXT = /^(?:personal|org:[A-Za-z0-9][A-Za-z0-9_-]{0,127})$/;
|
|
19
|
+
const AGENT_REF = /^urn:fabric:agent:[a-z][a-z0-9]*(?:-[a-z0-9]+)*:[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
20
|
+
const SLOT = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
21
|
+
const SLUG = /^[a-z][a-z0-9-]{1,62}$/;
|
|
22
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
23
|
+
const REQUIRED = ['type', 'alg', 'suite', 'recipient', 'signer', 'context', 'agentRef', 'slot', 'provider', 'sequence', 'iat', 'exp', 'enc'];
|
|
24
|
+
const ORG_ONLY = ['requestId', 'credentialExpiresAt'];
|
|
25
|
+
|
|
26
|
+
export class Refusal extends Error {
|
|
27
|
+
constructor(code, message) { super(message); this.code = code; }
|
|
28
|
+
}
|
|
29
|
+
class Transport extends Error {}
|
|
30
|
+
|
|
31
|
+
const when = (t) => Date.parse(t) / 1000;
|
|
32
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
33
|
+
const str = (v, re) => typeof v === 'string' && re.test(v);
|
|
34
|
+
|
|
35
|
+
export function envelopeProblems(envelope, want) {
|
|
36
|
+
if (!isObject(envelope) || Object.keys(envelope).sort().join() !== 'ct,header,sig') return ['not {header, ct, sig}'];
|
|
37
|
+
const h = envelope.header;
|
|
38
|
+
if (!isObject(h)) return ['header is not an object'];
|
|
39
|
+
let out = REQUIRED.filter((k) => !(k in h)).map((k) => `header misses ${k}`);
|
|
40
|
+
out = out.concat(Object.keys(h).filter((k) => !REQUIRED.includes(k) && !ORG_ONLY.includes(k)).sort().map((k) => `header has no member ${k}`));
|
|
41
|
+
if (out.length) return out;
|
|
42
|
+
const checks = [['type', h.type === ENVELOPE], ['suite', JSON.stringify(h.suite) === '[16,1,1]'],
|
|
43
|
+
['recipient', str(h.recipient, KEM)], ['signer', str(h.signer, SIGNER)], ['context', str(h.context, CONTEXT)],
|
|
44
|
+
['agentRef', str(h.agentRef, AGENT_REF) && h.agentRef.length <= 146], ['slot', str(h.slot, SLOT)],
|
|
45
|
+
['provider', str(h.provider, SLUG)], ['sequence', Number.isInteger(h.sequence) && h.sequence >= 1 && h.sequence <= 9007199254740991],
|
|
46
|
+
['iat', str(h.iat, TIME)], ['exp', str(h.exp, TIME)], ['enc', str(h.enc, B64URL)],
|
|
47
|
+
['ct', str(envelope.ct, B64URL)], ['sig', str(envelope.sig, SIG)]];
|
|
48
|
+
out = checks.filter(([, ok]) => !ok).map(([name]) => `${name} does not read`);
|
|
49
|
+
if (out.length) return out;
|
|
50
|
+
const org = h.context !== 'personal';
|
|
51
|
+
if (h.alg !== (org ? 'EdDSA' : 'ES256')) out.push("alg is EdDSA in an organization's context, ES256 in the personal one");
|
|
52
|
+
if (org && !ORG_ONLY.every((k) => k in h)) out.push("an organization's envelope names requestId and credentialExpiresAt");
|
|
53
|
+
if (!org && ORG_ONLY.some((k) => k in h)) out.push('a personal envelope names no requestId or credentialExpiresAt');
|
|
54
|
+
if (org && 'requestId' in h && !str(h.requestId, UUID)) out.push('requestId does not read');
|
|
55
|
+
const iat = when(h.iat);
|
|
56
|
+
const exp = when(h.exp);
|
|
57
|
+
if (!(exp - iat > 0 && exp - iat <= 24 * 3600)) out.push('exp is after iat and at most 24 hours later');
|
|
58
|
+
if (org && 'credentialExpiresAt' in h) {
|
|
59
|
+
const end = h.credentialExpiresAt;
|
|
60
|
+
if (!(str(end, TIME) && iat < when(end) && when(end) <= iat + 30 * 86400)) out.push('credentialExpiresAt is after iat and at most 30 days later');
|
|
61
|
+
}
|
|
62
|
+
for (const key of ['recipient', 'context', 'agentRef', 'slot', 'provider', 'requestId']) {
|
|
63
|
+
if (key in want && h[key] !== want[key]) out.push(`header ${key} is not the one asked for`);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const scrub = (text, value) => (value ? text.split(value).join('[value hidden]') : text);
|
|
69
|
+
|
|
70
|
+
function runChild(argv, payload, timeoutS) {
|
|
71
|
+
const done = spawnSync(argv[0], argv.slice(1), { input: payload, timeout: timeoutS * 1000, maxBuffer: 4 * MAX_ANSWER });
|
|
72
|
+
if (done.error?.code === 'ETIMEDOUT') throw new Transport(`${argv[0]} did not answer in ${timeoutS} s`);
|
|
73
|
+
if (done.error?.code === 'ENOBUFS') return [done.status ?? 1, Buffer.alloc(0)];
|
|
74
|
+
if (done.error) throw new Refusal(2, `${argv[0]} cannot be run: ${done.error.code ?? done.error.message}`);
|
|
75
|
+
if (done.stdout.length > MAX_ANSWER) return [done.status, Buffer.alloc(0)];
|
|
76
|
+
return [done.status, done.stdout];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parse(buf, code, message) {
|
|
80
|
+
try { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(buf)); } catch { throw new Refusal(code, message); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const commandIssuer = (argv, timeoutS) => (request) => {
|
|
84
|
+
const [code, out] = runChild(argv, JSON.stringify(request), timeoutS);
|
|
85
|
+
if (code === TRANSPORT_EXIT) throw new Transport('the issuer could not be reached');
|
|
86
|
+
if (code !== 0) throw new Refusal(3, `the issuer refused (exit ${code})`);
|
|
87
|
+
return parse(out, 4, "the issuer's answer is not JSON");
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const commandSealer = (argv, timeoutS) => (value, header) => {
|
|
91
|
+
let code; let out;
|
|
92
|
+
try { [code, out] = runChild(argv, JSON.stringify({ value, header }), timeoutS); } catch (err) {
|
|
93
|
+
if (err instanceof Transport) throw new Refusal(5, 'the sealer did not answer');
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
if (code !== 0) throw new Refusal(5, `the sealer refused (exit ${code})`);
|
|
97
|
+
return parse(out, 5, "the sealer's answer is not JSON");
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export function deliver(want, issue, seal, { sleep = (s) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, s * 1000) } = {}) {
|
|
101
|
+
const request = {};
|
|
102
|
+
for (const k of ['slot', 'provider', 'agentRef', 'context', 'requestId']) if (k in want) request[k] = want[k];
|
|
103
|
+
let answer;
|
|
104
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
105
|
+
try { answer = issue(request); break; } catch (err) {
|
|
106
|
+
if (!(err instanceof Transport)) throw err;
|
|
107
|
+
if (attempt === RETRIES) throw new Refusal(1, `transport failure after ${RETRIES + 1} tries: ${err.message}`);
|
|
108
|
+
sleep(attempt + 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (isObject(answer) && isObject(answer.refused)) {
|
|
112
|
+
const code = answer.refused.code;
|
|
113
|
+
throw new Refusal(3, `the issuer refused: ${str(code, SLUG) ? code : 'no code'}`);
|
|
114
|
+
}
|
|
115
|
+
const value = isObject(answer) ? answer.value : undefined;
|
|
116
|
+
if (typeof value !== 'string' || !value.trim() || /[\r\n]/.test(value)) throw new Refusal(4, "the issuer's answer holds no key");
|
|
117
|
+
const header = { type: ENVELOPE, recipient: want.recipient, context: want.context, agentRef: want.agentRef, slot: want.slot, provider: want.provider };
|
|
118
|
+
if (want.context !== 'personal') {
|
|
119
|
+
header.requestId = want.requestId;
|
|
120
|
+
if (typeof answer.credentialExpiresAt === 'string') header.credentialExpiresAt = answer.credentialExpiresAt;
|
|
121
|
+
}
|
|
122
|
+
const envelope = seal(value, header);
|
|
123
|
+
let problems = envelopeProblems(envelope, want);
|
|
124
|
+
const line = JSON.stringify(envelope);
|
|
125
|
+
if (!problems.length && line.includes(value)) problems = ['the envelope carries the value in clear'];
|
|
126
|
+
if (problems.length) throw new Refusal(5, `the sealed envelope is refused: ${problems.map((p) => scrub(p, value)).join('; ')}`);
|
|
127
|
+
return line;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function split(command) { // shlex.split's POSIX subset: quotes and backslashes
|
|
131
|
+
const out = []; let cur = ''; let quote = null; let any = false;
|
|
132
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
133
|
+
const c = command[i];
|
|
134
|
+
if (quote === "'") { if (c === "'") quote = null; else cur += c; continue; }
|
|
135
|
+
if (quote === '"') { if (c === '"') quote = null; else if (c === '\\' && i + 1 < command.length && '"\\$`'.includes(command[i + 1])) cur += command[++i]; else cur += c; continue; }
|
|
136
|
+
if (c === "'" || c === '"') { quote = c; any = true; } else if (c === '\\' && i + 1 < command.length) { cur += command[++i]; any = true; } else if (/\s/.test(c)) { if (any) out.push(cur); cur = ''; any = false; } else { cur += c; any = true; }
|
|
137
|
+
}
|
|
138
|
+
if (quote) throw new Error('No closing quotation');
|
|
139
|
+
if (any) out.push(cur);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function main(argv) {
|
|
144
|
+
let v;
|
|
145
|
+
try {
|
|
146
|
+
v = parseArgs({ args: argv, strict: true, options: Object.fromEntries(['slot', 'provider', 'agent-ref', 'recipient', 'context',
|
|
147
|
+
'issuer-command', 'seal-command', 'request-id', 'timeout'].map((k) => [k, { type: 'string' }])) }).values;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
process.stderr.write(`door.mjs: usage: ${err.message}\n`);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
const usage = [];
|
|
153
|
+
for (const k of ['slot', 'provider', 'agent-ref', 'recipient', 'context', 'issuer-command', 'seal-command']) if (v[k] === undefined) usage.push(`--${k} is required`);
|
|
154
|
+
if (usage.length) { process.stderr.write(`door.mjs: usage: ${usage.join(', ')}\n`); return 2; }
|
|
155
|
+
const want = { slot: v.slot, provider: v.provider, agentRef: v['agent-ref'], recipient: v.recipient, context: v.context };
|
|
156
|
+
for (const [name, re, value] of [['--slot', SLOT, v.slot], ['--provider', SLUG, v.provider], ['--agent-ref', AGENT_REF, v['agent-ref']],
|
|
157
|
+
['--recipient', KEM, v.recipient], ['--context', CONTEXT, v.context]]) if (!re.test(value)) usage.push(name);
|
|
158
|
+
if (v.context !== 'personal') {
|
|
159
|
+
if (!str(v['request-id'], UUID)) usage.push("--request-id (an organization's context answers a credential-request/1)");
|
|
160
|
+
want.requestId = v['request-id'];
|
|
161
|
+
} else if (v['request-id']) usage.push('--request-id (a personal envelope names none)');
|
|
162
|
+
const timeout = v.timeout === undefined ? 60 : Number(v.timeout);
|
|
163
|
+
if (!(timeout > 0)) usage.push('--timeout');
|
|
164
|
+
let issuer = []; let sealer = [];
|
|
165
|
+
try { issuer = split(v['issuer-command']); sealer = split(v['seal-command']); } catch (err) { usage.push(`a command that does not split: ${err.message}`); }
|
|
166
|
+
if (usage.length || !issuer.length || !sealer.length) {
|
|
167
|
+
process.stderr.write(`door.mjs: usage: ${(usage.length ? usage : ['an empty command']).join(', ')}\n`);
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
process.stdout.write(`${deliver(want, commandIssuer(issuer, timeout), commandSealer(sealer, timeout))}\n`);
|
|
172
|
+
return 0;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (!(err instanceof Refusal)) throw err;
|
|
175
|
+
process.stderr.write(`door.mjs: ${err.message}\n`);
|
|
176
|
+
return err.code;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const invoked = (() => { try { return fs.realpathSync(process.argv[1] ?? ''); } catch { return null; } })();
|
|
181
|
+
if (invoked === fileURLToPath(import.meta.url)) process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A key-delivery door template (FAA-238): ask an issuer for one key, print it sealed as the DEC-0111 envelope.
|
|
3
|
+
|
|
4
|
+
python3 door.py --slot NAME --provider SLUG --agent-ref URN --recipient KID#kem --context personal|org:<id>
|
|
5
|
+
[--request-id UUID] --issuer-command "CMD ..." --seal-command "CMD ..." [--timeout 60]
|
|
6
|
+
|
|
7
|
+
The door prints exactly one `fabric-credential-envelope/1` (contract DEC-0111) on stdout and writes into no store: the
|
|
8
|
+
receiver puts it in the recipient's slot (Project Observatory's `vault.py receive PROJECT ENV NAME --from-command --
|
|
9
|
+
door.py ...`, its V2 spec, until its OBS-360 ships). The value is never in argv, the environment, a file or stderr.
|
|
10
|
+
|
|
11
|
+
The two commands are the door owner's (a value travels only over their stdin and stdout, held in memory):
|
|
12
|
+
- the issuer command reads the request (`{slot, provider, agentRef, context, requestId?}`, no secret) on stdin and
|
|
13
|
+
prints `{"value": "<key>", "credentialExpiresAt"?: "<RFC 3339>"}` or `{"refused": {"code": "<code>"}}`; exit 75
|
|
14
|
+
(EX_TEMPFAIL) is a transport failure, any other non-zero exit a refusal;
|
|
15
|
+
- the seal command reads `{"value", "header"}` on stdin (the header's fields the door fixes) and prints the envelope,
|
|
16
|
+
sealed with HPKE and signed as DEC-0111 says (the contract's `sealCredential`); the standard library seals nothing.
|
|
17
|
+
|
|
18
|
+
Exit codes, as Observatory's `receive --from-command` maps them: 0 delivered (the envelope on stdout), 2 usage,
|
|
19
|
+
3 the issuer refused, 4 no key in the issuer's answer, 5 the store refused (the envelope cannot be sealed for the
|
|
20
|
+
recipient's store: the sealer refused, or its envelope fails the checks below), 1 a transport failure that outlived
|
|
21
|
+
its retries. Only a transport failure is retried, at most twice.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import datetime as _dt
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import shlex
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
|
35
|
+
|
|
36
|
+
ENVELOPE = "fabric-credential-envelope/1"
|
|
37
|
+
MAX_ANSWER = 65536
|
|
38
|
+
RETRIES = 2
|
|
39
|
+
TRANSPORT_EXIT = 75
|
|
40
|
+
_TIME = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?Z\Z")
|
|
41
|
+
_B64URL = re.compile(r"^[A-Za-z0-9_-]+\Z")
|
|
42
|
+
_SIG = re.compile(r"^[A-Za-z0-9_-]{86}\Z")
|
|
43
|
+
_KEM = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}#kem\Z")
|
|
44
|
+
_SIGNER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}(?:#sig)?\Z")
|
|
45
|
+
_CONTEXT = re.compile(r"^(?:personal|org:[A-Za-z0-9][A-Za-z0-9_-]{0,127})\Z")
|
|
46
|
+
_AGENT_REF = re.compile(r"^urn:fabric:agent:[a-z][a-z0-9]*(?:-[a-z0-9]+)*:[a-z][a-z0-9]*(?:-[a-z0-9]+)*\Z")
|
|
47
|
+
_SLOT = re.compile(r"^[A-Z][A-Z0-9_]{0,63}\Z")
|
|
48
|
+
_SLUG = re.compile(r"^[a-z][a-z0-9-]{1,62}\Z")
|
|
49
|
+
_UUID = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z")
|
|
50
|
+
_REQUIRED = ("type", "alg", "suite", "recipient", "signer", "context", "agentRef", "slot", "provider", "sequence",
|
|
51
|
+
"iat", "exp", "enc")
|
|
52
|
+
_ORG_ONLY = ("requestId", "credentialExpiresAt")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Refusal(Exception):
|
|
56
|
+
"""An exit the door answers with: code and a message that never holds the value."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, code: int, message: str):
|
|
59
|
+
super().__init__(message)
|
|
60
|
+
self.code = code
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Transport(Exception):
|
|
64
|
+
"""The issuer could not be reached: the one failure worth another try."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _when(text: str) -> float:
|
|
68
|
+
return _dt.datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def envelope_problems(envelope: Any, want: Dict[str, Any]) -> List[str]:
|
|
72
|
+
"""What keeps an envelope from the DEC-0111 shape (credential-envelope.schema.json at the adapter's pin) and from the
|
|
73
|
+
request it answers. Never decrypts or verifies a signature: that is the target's checkCredentialEnvelope."""
|
|
74
|
+
if not isinstance(envelope, dict) or set(envelope) != {"header", "ct", "sig"}:
|
|
75
|
+
return ["not {header, ct, sig}"]
|
|
76
|
+
h, out = envelope["header"], []
|
|
77
|
+
if not isinstance(h, dict):
|
|
78
|
+
return ["header is not an object"]
|
|
79
|
+
out += ["header misses %s" % k for k in _REQUIRED if k not in h]
|
|
80
|
+
out += ["header has no member %s" % k for k in sorted(set(h) - set(_REQUIRED) - set(_ORG_ONLY))]
|
|
81
|
+
if out:
|
|
82
|
+
return out
|
|
83
|
+
checks = [("type", h["type"] == ENVELOPE), ("suite", h["suite"] == [16, 1, 1]),
|
|
84
|
+
("recipient", isinstance(h["recipient"], str) and bool(_KEM.match(h["recipient"]))),
|
|
85
|
+
("signer", isinstance(h["signer"], str) and bool(_SIGNER.match(h["signer"]))),
|
|
86
|
+
("context", isinstance(h["context"], str) and bool(_CONTEXT.match(h["context"]))),
|
|
87
|
+
("agentRef", isinstance(h["agentRef"], str) and len(h["agentRef"]) <= 146 and bool(_AGENT_REF.match(h["agentRef"]))),
|
|
88
|
+
("slot", isinstance(h["slot"], str) and bool(_SLOT.match(h["slot"]))),
|
|
89
|
+
("provider", isinstance(h["provider"], str) and bool(_SLUG.match(h["provider"]))),
|
|
90
|
+
("sequence", type(h["sequence"]) is int and 1 <= h["sequence"] <= 9007199254740991),
|
|
91
|
+
("iat", isinstance(h["iat"], str) and bool(_TIME.match(h["iat"]))),
|
|
92
|
+
("exp", isinstance(h["exp"], str) and bool(_TIME.match(h["exp"]))),
|
|
93
|
+
("enc", isinstance(h["enc"], str) and bool(_B64URL.match(h["enc"]))),
|
|
94
|
+
("ct", isinstance(envelope["ct"], str) and bool(_B64URL.match(envelope["ct"]))),
|
|
95
|
+
("sig", isinstance(envelope["sig"], str) and bool(_SIG.match(envelope["sig"])))]
|
|
96
|
+
out += ["%s does not read" % name for name, ok in checks if not ok]
|
|
97
|
+
if out:
|
|
98
|
+
return out
|
|
99
|
+
org = h["context"] != "personal"
|
|
100
|
+
if h["alg"] != ("EdDSA" if org else "ES256"):
|
|
101
|
+
out.append("alg is EdDSA in an organization's context, ES256 in the personal one")
|
|
102
|
+
if org and not all(k in h for k in _ORG_ONLY):
|
|
103
|
+
out.append("an organization's envelope names requestId and credentialExpiresAt")
|
|
104
|
+
if not org and any(k in h for k in _ORG_ONLY):
|
|
105
|
+
out.append("a personal envelope names no requestId or credentialExpiresAt")
|
|
106
|
+
if org and "requestId" in h and not (isinstance(h["requestId"], str) and _UUID.match(h["requestId"])):
|
|
107
|
+
out.append("requestId does not read")
|
|
108
|
+
iat, exp = _when(h["iat"]), _when(h["exp"])
|
|
109
|
+
if not 0 < exp - iat <= 24 * 3600:
|
|
110
|
+
out.append("exp is after iat and at most 24 hours later")
|
|
111
|
+
if org and "credentialExpiresAt" in h:
|
|
112
|
+
end = h["credentialExpiresAt"]
|
|
113
|
+
if not (isinstance(end, str) and _TIME.match(end) and iat < _when(end) <= iat + 30 * 86400):
|
|
114
|
+
out.append("credentialExpiresAt is after iat and at most 30 days later")
|
|
115
|
+
for key in ("recipient", "context", "agentRef", "slot", "provider", "requestId"):
|
|
116
|
+
if key in want and h.get(key) != want[key]:
|
|
117
|
+
out.append("header %s is not the one asked for" % key)
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def scrub(text: str, value: Optional[str]) -> str:
|
|
122
|
+
return text.replace(value, "[value hidden]") if value else text
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_child(argv: Sequence[str], payload: bytes, timeout: float) -> Tuple[int, bytes, str]:
|
|
126
|
+
"""A child with the payload on stdin; its stdout in memory (at most 64 KiB), its stderr as text."""
|
|
127
|
+
try:
|
|
128
|
+
done = subprocess.run(list(argv), input=payload, capture_output=True, timeout=timeout)
|
|
129
|
+
except subprocess.TimeoutExpired:
|
|
130
|
+
raise Transport("%s did not answer in %g s" % (argv[0], timeout))
|
|
131
|
+
except OSError as exc:
|
|
132
|
+
raise Refusal(2, "%s cannot be run: %s" % (argv[0], exc.strerror or exc))
|
|
133
|
+
if len(done.stdout) > MAX_ANSWER:
|
|
134
|
+
return done.returncode, b"", "its answer is over %d bytes" % MAX_ANSWER
|
|
135
|
+
return done.returncode, done.stdout, done.stderr.decode("utf-8", "replace")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def command_issuer(argv: Sequence[str], timeout: float) -> Callable[[Dict[str, Any]], Dict[str, Any]]:
|
|
139
|
+
def issue(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
140
|
+
code, out, err = run_child(argv, json.dumps(request).encode(), timeout)
|
|
141
|
+
if code == TRANSPORT_EXIT:
|
|
142
|
+
raise Transport("the issuer could not be reached")
|
|
143
|
+
if code != 0:
|
|
144
|
+
raise Refusal(3, "the issuer refused (exit %d)" % code)
|
|
145
|
+
try:
|
|
146
|
+
return json.loads(out.decode("utf-8"))
|
|
147
|
+
except (ValueError, UnicodeDecodeError):
|
|
148
|
+
raise Refusal(4, "the issuer's answer is not JSON")
|
|
149
|
+
return issue
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def command_sealer(argv: Sequence[str], timeout: float) -> Callable[[str, Dict[str, Any]], Any]:
|
|
153
|
+
def seal(value: str, header: Dict[str, Any]) -> Any:
|
|
154
|
+
try:
|
|
155
|
+
code, out, _err = run_child(argv, json.dumps({"value": value, "header": header}).encode(), timeout)
|
|
156
|
+
except Transport:
|
|
157
|
+
raise Refusal(5, "the sealer did not answer")
|
|
158
|
+
if code != 0:
|
|
159
|
+
raise Refusal(5, "the sealer refused (exit %d)" % code)
|
|
160
|
+
try:
|
|
161
|
+
return json.loads(out.decode("utf-8"))
|
|
162
|
+
except (ValueError, UnicodeDecodeError):
|
|
163
|
+
raise Refusal(5, "the sealer's answer is not JSON")
|
|
164
|
+
return seal
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def deliver(want: Dict[str, Any], issue: Callable[[Dict[str, Any]], Dict[str, Any]],
|
|
168
|
+
seal: Callable[[str, Dict[str, Any]], Any], *, sleep: Callable[[float], None] = time.sleep) -> str:
|
|
169
|
+
"""The envelope's one line, or a Refusal. `want` names slot, provider, agentRef, recipient, context, requestId?."""
|
|
170
|
+
request = {k: want[k] for k in ("slot", "provider", "agentRef", "context", "requestId") if k in want}
|
|
171
|
+
for attempt in range(RETRIES + 1):
|
|
172
|
+
try:
|
|
173
|
+
answer = issue(request)
|
|
174
|
+
break
|
|
175
|
+
except Transport as exc:
|
|
176
|
+
if attempt == RETRIES:
|
|
177
|
+
raise Refusal(1, "transport failure after %d tries: %s" % (RETRIES + 1, exc))
|
|
178
|
+
sleep(1.0 * (attempt + 1))
|
|
179
|
+
if isinstance(answer, dict) and isinstance(answer.get("refused"), dict):
|
|
180
|
+
code = answer["refused"].get("code")
|
|
181
|
+
raise Refusal(3, "the issuer refused: %s" % (code if isinstance(code, str) and _SLUG.match(code) else "no code"))
|
|
182
|
+
value = answer.get("value") if isinstance(answer, dict) else None
|
|
183
|
+
if not isinstance(value, str) or not value.strip() or "\n" in value or "\r" in value:
|
|
184
|
+
raise Refusal(4, "the issuer's answer holds no key")
|
|
185
|
+
header = {"type": ENVELOPE, "recipient": want["recipient"], "context": want["context"], "agentRef": want["agentRef"],
|
|
186
|
+
"slot": want["slot"], "provider": want["provider"]}
|
|
187
|
+
if want["context"] != "personal":
|
|
188
|
+
header["requestId"] = want["requestId"]
|
|
189
|
+
if isinstance(answer.get("credentialExpiresAt"), str):
|
|
190
|
+
header["credentialExpiresAt"] = answer["credentialExpiresAt"]
|
|
191
|
+
envelope = seal(value, header)
|
|
192
|
+
problems = envelope_problems(envelope, want)
|
|
193
|
+
line = json.dumps(envelope, separators=(",", ":"))
|
|
194
|
+
if not problems and value in line:
|
|
195
|
+
problems = ["the envelope carries the value in clear"]
|
|
196
|
+
if problems:
|
|
197
|
+
raise Refusal(5, "the sealed envelope is refused: %s" % "; ".join(scrub(p, value) for p in problems))
|
|
198
|
+
return line
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
202
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
203
|
+
for flag in ("--slot", "--provider", "--agent-ref", "--recipient", "--context", "--issuer-command", "--seal-command"):
|
|
204
|
+
parser.add_argument(flag, required=True)
|
|
205
|
+
parser.add_argument("--request-id")
|
|
206
|
+
parser.add_argument("--timeout", type=float, default=60.0)
|
|
207
|
+
args = parser.parse_args(argv)
|
|
208
|
+
want = {"slot": args.slot, "provider": args.provider, "agentRef": args.agent_ref, "recipient": args.recipient,
|
|
209
|
+
"context": args.context}
|
|
210
|
+
usage = [name for name, pattern, value in (("--slot", _SLOT, args.slot), ("--provider", _SLUG, args.provider),
|
|
211
|
+
("--agent-ref", _AGENT_REF, args.agent_ref), ("--recipient", _KEM, args.recipient),
|
|
212
|
+
("--context", _CONTEXT, args.context)) if not pattern.match(value)]
|
|
213
|
+
if args.context != "personal":
|
|
214
|
+
if not (args.request_id and _UUID.match(args.request_id)):
|
|
215
|
+
usage.append("--request-id (an organization's context answers a credential-request/1)")
|
|
216
|
+
want["requestId"] = args.request_id
|
|
217
|
+
elif args.request_id:
|
|
218
|
+
usage.append("--request-id (a personal envelope names none)")
|
|
219
|
+
try:
|
|
220
|
+
issuer, sealer = shlex.split(args.issuer_command), shlex.split(args.seal_command)
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
usage.append("a command that does not split: %s" % exc)
|
|
223
|
+
issuer = sealer = []
|
|
224
|
+
if usage or not issuer or not sealer:
|
|
225
|
+
print("door.py: usage: %s" % ", ".join(usage or ["an empty command"]), file=sys.stderr)
|
|
226
|
+
return 2
|
|
227
|
+
try:
|
|
228
|
+
line = deliver(want, command_issuer(issuer, args.timeout), command_sealer(sealer, args.timeout))
|
|
229
|
+
except Refusal as exc:
|
|
230
|
+
print("door.py: %s" % exc, file=sys.stderr)
|
|
231
|
+
return exc.code
|
|
232
|
+
sys.stdout.write(line + "\n")
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
raise SystemExit(main())
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A service answers one inbound question from its own knowledge file, or hands it to a person. It is a step of an
|
|
4
4
|
agent that answers people, not that agent: see the paragraph after the table. Under the creating-fabric-agents
|
|
5
|
-
template (`communication-agent-template.md`) the plan-and-draft call writes the draft, not this recipe's `answer()`:
|
|
5
|
+
template (`templates/communication-agent/`, read through `references/communication-agent-template.md`) the plan-and-draft call writes the draft, not this recipe's `answer()`:
|
|
6
6
|
the draft carries a `[KB:<id>]` marker for each knowledge id the plan used, and the agent runs `check_reply(draft,
|
|
7
7
|
question, prompt_ids, max_chars=..., sources=...)` on it for the code checks. `prompt_ids` are the ids of the items
|
|
8
8
|
placed in the plan's prompt, never the plan's own list (an id the plan names outside them is held as a suspected
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md
CHANGED
|
@@ -23,11 +23,11 @@ an audit names its subject, the agent's Project id or its repository's remote, n
|
|
|
23
23
|
a contribution carries one success case or one decision (below). No request carries source code, a key, a person's or
|
|
24
24
|
client's name, user content or a private path.
|
|
25
25
|
|
|
26
|
-
Send it only from a folder that is already a Fabric Project; from any other folder, skip the ask. Where several
|
|
27
|
-
Projects list the capability, pick none and go on without the reply, saying so in the report. Here this reference wins over every working-with-fabric rule (its `references/project-board.md` and its SKILL.md) that
|
|
26
|
+
Send it only from a folder that is already a Fabric Project (a folder `fabric_whoami` would admit on the way to the ask is not one); from any other folder, skip the ask. A run Fabric started on a board request sends no ask: its retro is its last write, so its measured win stays in its insight lines for a later session to contribute. Where several
|
|
27
|
+
Projects list the capability, or `com.directory` lists the capability with no `active` responder (a request then waits for a run that may decline it and ask the operator), pick none and go on without the reply, saying so in the report. Here this reference wins over every working-with-fabric rule (its `references/project-board.md` and its SKILL.md) that
|
|
28
28
|
would make this ask resubmit, adapt the work itself, escalate or ask the person: the agent asks no one, uses no
|
|
29
|
-
Project id held from an earlier thread or from the operator, and an unanswered request is
|
|
30
|
-
deadline. A decline, including one from a run Fabric started, is read as data like any reply; a question that run
|
|
29
|
+
Project id held from an earlier thread or from the operator, and an unanswered request is cancelled (`com.cancel`) once the step it served is reported: a queued request
|
|
30
|
+
expires at its deadline, but an accepted one never expires. A decline, including one from a run Fabric started, is read as data like any reply; a question that run
|
|
31
31
|
puts to the operator is Fabric's, not this agent's. The ask never blocks the step it serves; send it and go on without waiting for the reply. With no Fabric or board
|
|
32
32
|
reachable, a board that does not serve `com.directory`, no Project listed under the capability, or a receipt that says
|
|
33
33
|
`noResponder`, note the request id (or that none could be sent), go on without a template or an audit, and say so in
|
|
@@ -39,7 +39,8 @@ refuses it with `invalid_arguments` (FAA-244).
|
|
|
39
39
|
3. Each connection joins with `fabric.board.join {projectId}` (`join_board` / `joinBoard`). It re-reads `hub.json`,
|
|
40
40
|
checks the pid it names is alive, tells the owner of the door's listener from the socket table (DEC-0154 HA-2:
|
|
41
41
|
another OS account's process, or one whose owner cannot be told, is `HubError` code `foreign-user`, retryable, with
|
|
42
|
-
nothing sent; a
|
|
42
|
+
nothing sent; a restricted macOS table, which a retry never clears, is `data.reason` `table-restricted` and not
|
|
43
|
+
retryable, protocol.md, FAA-272), and only then sends the credential (a session already running carries that refusal as `BoardError` code
|
|
43
44
|
`foreign-user`, one of `SESSION_CODES`, to `board.down` with reason `foreign-user` and a sentence that says nothing
|
|
44
45
|
was sent, never `unreachable`'s "a write may have been stored", FAA-252). A missing grant is `HubError` code
|
|
45
46
|
`not_authorized` whose `data.ask` holds the arguments to ask with.
|
|
@@ -214,7 +215,8 @@ capability's enroll or replace is settled on its own: a refusal of that capabili
|
|
|
214
215
|
`invalid_arguments`, any code that is not about the board session) leaves it `retry`, asked again alone after 15 s,
|
|
215
216
|
30 s, doubling to 15 minutes, with one `board.slot.refused` event (warning; said again if it is refused again after
|
|
216
217
|
the slot was held); its accepted work waits `lapsed` and goes back
|
|
217
|
-
by a transfer once the slot is held
|
|
218
|
+
by a transfer once the slot is held; `idempotency_window_expired` also reads `com.status` first, so the retry goes
|
|
219
|
+
under the epoch Fabric names now (FAA-186). The capabilities after it are recovered as before. A refusal of the board session
|
|
218
220
|
(`unreachable`, `session-ended`, `not_available`, `not_authorized` and the other codes in `SESSION_CODES`) leaves the
|
|
219
221
|
rest `retry` too, says no `board.slot.refused`, and goes to the responder's fault, which says it by code:
|
|
220
222
|
`board.access.needed` for `not_authorized`, `authority_changed` and `credential` (the session is left),
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md
CHANGED
|
@@ -28,7 +28,7 @@ other browser, unless Fabric Dashboards is confirmed not installed.
|
|
|
28
28
|
is also authoritative. Hand back **the returned `open_link` as the primary
|
|
29
29
|
dashboard action**. Do not rebuild its URI or double-encode `path`.
|
|
30
30
|
3. Use `host_status`, where offered, to distinguish available, not_installed,
|
|
31
|
-
unknown, incompatible and
|
|
31
|
+
unknown, incompatible, handler_mismatch and unsupported. It is a read, not a GUI launch.
|
|
32
32
|
With an older server lacking that tool, host readiness is unverified: use a
|
|
33
33
|
compatible host resolver or report the limitation, not presumed absence.
|
|
34
34
|
4. A request for a link opens nothing. A request to open uses the host `open`
|
|
@@ -65,8 +65,8 @@ host's `open` with `fallback=never`. Never `open <url>`, `open -a Safari`,
|
|
|
65
65
|
agent's pages. A browser only when `host_status` confirms `not_installed`, and
|
|
66
66
|
a raw `http://127.0.0.1:<port>` address only after the listener on that port is
|
|
67
67
|
this OS account's (DEC-0154 PL-3 and HA-2; the adapter's `building-fabric-services/scripts/fabric_peer_owner.py`: `server_end` of a connection it opens and sends nothing on; `check_service.py <id>.<instance>` prints it as `listener.owner`): a
|
|
68
|
-
`foreign-user` listener is reported, never opened (the link gate below
|
|
69
|
-
|
|
68
|
+
`foreign-user` listener is reported, never opened (the link gate below takes
|
|
69
|
+
that owner as `listener_owner`). An installed host that failed
|
|
70
70
|
to open is reported, not replaced by HTTP. Links
|
|
71
71
|
carry no token or login code. Every completion and notification action passes
|
|
72
72
|
the Fabric Agent Adapter's link gate before delivery: `check_dashboard_link.py`
|
|
@@ -119,7 +119,15 @@ Its trusted context is supplied by the host, not by untrusted model output:
|
|
|
119
119
|
|
|
120
120
|
`native_link` must equal the resolved open_link, with an available host on the
|
|
121
121
|
viewer's device. `browser_link` must equal http_url, with confirmed not_installed,
|
|
122
|
-
if_absent policy
|
|
122
|
+
if_absent policy, the same device and `"listener_owner": "this-account"`: the kind
|
|
123
|
+
the host read for the listener on http_url's port (`fabric_peer_owner`'s
|
|
124
|
+
`this-account`, `another-account`, `system`, `unknown`, `none`, or
|
|
125
|
+
`check_service.py`'s `foreign-user`); any other, or none given, refuses the
|
|
126
|
+
browser (DEC-0154 PL-3, FAA-251). The native link does not take it: Fabric
|
|
127
|
+
Dashboards checks its own listener. Contract DEC-0123 PL-3 also allows a browser
|
|
128
|
+
on `unsupported` (a system the host does not ship for); the gate treats
|
|
129
|
+
`unsupported` as no fallback, by the operator's rule that a browser opens only on a
|
|
130
|
+
confirmed `not_installed`, and macOS never reports it (FAA-137). `remote_open` instead contains `device_id`,
|
|
123
131
|
`service` and optional `path`, exactly matching the target and resolved page;
|
|
124
132
|
its acceptance does not authorize or deliver it. Enforce grants/dedup/expiry in
|
|
125
133
|
the transport. Unknown or failed host status produces no executable local action.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md
CHANGED
|
@@ -53,8 +53,11 @@ the same owner; **T2** everyone else, and any call relayed onto loopback from of
|
|
|
53
53
|
hub forwarding a T2 caller). DL-26: a T0 or T1 caller is never required to present a token; pass the tier,
|
|
54
54
|
`gate(tool, headers, tier="T0")` or `server.handle(message, headers, tier="T0")`, and the call passes with no
|
|
55
55
|
delegation. On a computer with more than one counted OS account, a loopback caller with no token is T0 only after the
|
|
56
|
-
door checks the peer's account (the pinned DEC-0154 HA-3 and DL-25
|
|
57
|
-
`
|
|
56
|
+
door checks the peer's account (the pinned DEC-0154 HA-3 and DL-25): the kit's `LoopbackHTTPServer` with `peer_check="strict"` (Node
|
|
57
|
+
`loopbackServer(app, {peerCheck: 'strict'})`, [protocol.md](protocol.md#the-listeners-owner-dec-0154-ha-2)) is that check,
|
|
58
|
+
for loopback peers: pass `tier="T0"` only when the handler's `self.server.peer_owner()` (`connectionOwner(req.socket)`)
|
|
59
|
+
names an owner, since a peer off loopback is served without one and is T2;
|
|
60
|
+
otherwise pass `tier="T0"` there only for a caller the descriptor token authenticated. A token such a caller presents is still verified and refused when invalid. A T2 caller takes the whole
|
|
58
61
|
profile. Money limits and human gates bind at every tier.
|
|
59
62
|
|
|
60
63
|
**The family and the agent network** (DEC-0130, wording settled by DEC-0137). The operator's family is every agent
|
|
@@ -163,7 +166,8 @@ current verified revision as `estate=` / `{ estate }`, as a value or as a functi
|
|
|
163
166
|
- **Which hub key a token is checked against** (DL-14a, DEC-0128). An organization's tokens are signed by that
|
|
164
167
|
organization context's own hub key (`machines[].contextHubKeys`), under the context issuer its pin names
|
|
165
168
|
(`organizations[].iss`, `urn:fabric:context:<opaque>`, never the estate id). An instance that serves an organization
|
|
166
|
-
passes its context as `estate_context=` / `{ estateContext }` to `delegation_gate` or `verify_delegation
|
|
169
|
+
passes its context as `estate_context=` / `{ estateContext }` to `delegation_gate` or `verify_delegation`
|
|
170
|
+
(`fabric_context.context_gate_options` hands it and the pin's `iss` back, FAA-150), once the
|
|
167
171
|
record pins its organization (until then its tokens are refused `kid-not-certified`, brief D5), and that pin's `iss`
|
|
168
172
|
as `issuer`: the `kid` is then trusted only when one machine lists it for that context with that key and
|
|
169
173
|
the record pins the organization. Without it, or with `personal`, the personal `hubKey` is read, so a context key
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md
CHANGED
|
@@ -98,6 +98,15 @@ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=pin["iss"], effects=..
|
|
|
98
98
|
estate_context="org:acme-rehearsal") # the context fabric_context started it for
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
`fabric_context.context_gate_options(info, record)` / `contextGateOptions(info, record)` hands both back from the
|
|
102
|
+
instance `hold_context_instance` started (FAA-150): `estate_context` always, and `issuer` when the record pins that
|
|
103
|
+
organization; spread it after the estate issuer, so a personal instance keeps the estate's:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
gate = fd.delegation_gate(audience=..., jwks=keys, effects=..., estate=lambda: current,
|
|
107
|
+
**{"issuer": estate_issuer, **fabric_context.context_gate_options(info, current)})
|
|
108
|
+
```
|
|
109
|
+
|
|
101
110
|
The `kid` is then certified only when exactly one machine lists it for that context with the published key and the
|
|
102
111
|
revision pins the organization; omitted or `personal`, the personal `hubKey` is read. A context key never verifies a
|
|
103
112
|
personal token, the personal key never an organization's, and a context that is neither (`Org:x`, a trailing space, a
|
|
@@ -218,7 +227,9 @@ const { dirs, instance, lock } = await holdContextInstance('example-agent', 'org
|
|
|
218
227
|
kept; another context or instance, or unreadable, releases the lock and refuses. The folder never changes context.
|
|
219
228
|
|
|
220
229
|
A refusal is one sentence on stderr naming the code and the context, never a record or grant value, and exit 78
|
|
221
|
-
(`EXIT_CONTEXT_REFUSED`, EX_CONFIG; 75 stays "already running")
|
|
230
|
+
(`EXIT_CONTEXT_REFUSED`, EX_CONFIG; 75 stays "already running"); a copy its supervisor started waits the lock's
|
|
231
|
+
back-off (300 s in all) before that exit, so `KeepAlive` does not restart it every 10 s (FAA-279). `ContextRefused`
|
|
232
|
+
carries the code:
|
|
222
233
|
|
|
223
234
|
| Code | When |
|
|
224
235
|
|---|---|
|