@passioncode-ai/fabric-agent-adapter 0.51.1 → 0.52.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +4 -1
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +3 -2
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +22 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +44 -6
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +46 -8
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -4
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +7 -2
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +30 -15
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +5 -0
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +17 -9
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +606 -0
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +48 -525
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +43 -6
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +14 -4
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +48 -15
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +4 -3
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +39 -16
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +8 -1
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -11
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +86 -642
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +766 -0
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +70 -12
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +59 -8
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +56 -16
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +4 -2
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +220 -21
  35. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  36. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +4 -1
  37. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -0
  38. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +69 -34
  39. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +27 -15
  40. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +13 -7
  41. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +8 -0
  42. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -9
  43. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +60 -1
  44. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +10 -3
  45. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +39 -4
@@ -9,19 +9,22 @@
9
9
  // - actionsExtensionRules(wellKnown), actionRules(kind, value): FAC-SEM-142.
10
10
  // - actionsExtension(actions, { capabilities }): the `extensions` entry a service declares; it refuses a declaration
11
11
  // FAC-SEM-142 would find, so a service never publishes one.
12
- // - operatorDecision({action, instruction, at, target}): the decision an agent relays in _meta.decision (AP-2, AP-3),
13
- // refused when decision.schema.json would refuse it.
12
+ // - operatorDecision({action, instruction, at, target, source}): the decision an agent relays in _meta.decision (AP-2,
13
+ // AP-3), refused when decision.schema.json would refuse it.
14
14
  // - actionCaller(tier, authenticated): the caller a service records (AP-2): the identity it authenticated, else
15
15
  // local:T0 / local:T1 for a caller its local floor authenticated, never a name the call carries.
16
16
  // - DEC-0125: actionDecision reads a call's mandate (_meta.mandate) through fabric-mandate.mjs's mandateCovers when the
17
17
  // call carries no decision.
18
+ // - DEC-0126: a decision may carry `source` (where the operator said the words, OA-2); actionDecision returns it in
19
+ // recordAs (OA-6). A service that reads it lists operator-source/1 and keeps it, as `instruction`, in its own audit
20
+ // record only: never an event, a usage report, a log line or a page another party reads (OA-7).
18
21
  //
19
22
  // The kit has no JSON Schema validator: the two shapes are written here from the schemas, lengths in code points and
20
23
  // patterns over the whole string, as the contract's validator reads them. Members are an object's own members (a JSON
21
24
  // text has no other kind; an inherited member is not read). Never throws on what it reads. Node 20+, no dependency.
22
25
  import { ServiceError } from './fabric-base.mjs';
23
26
  import { isObject, jsonEqual } from './fabric-jsvalue.mjs';
24
- import { mandateCovers } from './fabric-mandate.mjs'; // DEC-0125
27
+ import { mandateCovers, sourceShape } from './fabric-mandate.mjs'; // DEC-0125, DEC-0126
25
28
  import { formatRfc3339, parseRfc3339Ms } from './fabric-time.mjs';
26
29
 
27
30
  export const ACTIONS_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/actions/0.1';
@@ -43,7 +46,7 @@ const CAPABILITY = /^[a-z][a-z0-9._-]{1,127}$/u; // common.schema.json#/$defs/ca
43
46
  const SHA256 = /^sha256:[0-9a-f]{64}$/;
44
47
  const STEP_URL = /^(https:\/\/|\/)/;
45
48
  const ACTION_MEMBERS = new Set(['id', 'title', 'tool', 'gate', 'humanStep']);
46
- const DECISION_MEMBERS = new Set(['by', 'via', 'action', 'target', 'instruction', 'at']);
49
+ const DECISION_MEMBERS = new Set(['by', 'via', 'action', 'target', 'instruction', 'at', 'source']);
47
50
  const KEY_PATH = `/extensions/${ACTIONS_EXTENSION_KEY.replace(/~/g, '~0').replace(/\//g, '~1')}`;
48
51
 
49
52
  /** An own member that is not undefined, as the validator reads a member as present. */
@@ -91,15 +94,17 @@ export const readableAction = (action) => actionErrors(action, '').length === 0
91
94
 
92
95
  /**
93
96
  * decision.schema.json (DEC-0121 AP-2): by `operator`, via `relayed-instruction`, the action's id, an optional target
94
- * of 1..200, the operator's words of 1..500, and `at` read strictly (the contract's strictTime; ajv-formats' date-time
95
- * also admits a space for T and a leap second, which this refuses, decision D5).
97
+ * of 1..200, the operator's words of 1..500, `at` read strictly (the contract's strictTime; ajv-formats' date-time also
98
+ * admits a space for T and a leap second, which this refuses, decision D5), and an optional `source` as
99
+ * mandate.schema.json#/$defs/source reads it (DEC-0126).
96
100
  */
97
101
  export function decisionShape(value) {
98
102
  if (!isObject(value) || Object.keys(value).some((key) => !DECISION_MEMBERS.has(key))) return false;
99
103
  if (['by', 'via', 'action', 'instruction', 'at'].some((key) => !has(value, key))) return false;
100
104
  return value.by === 'operator' && value.via === 'relayed-instruction' && matches(REQUEST_KEY, value.action, 64)
101
105
  && (!has(value, 'target') || text(value.target, 1, 200)) && text(value.instruction, 1, 500)
102
- && typeof value.at === 'string' && !Number.isNaN(parseRfc3339Ms(value.at));
106
+ && typeof value.at === 'string' && !Number.isNaN(parseRfc3339Ms(value.at))
107
+ && (!has(value, 'source') || sourceShape(value.source));
103
108
  }
104
109
 
105
110
  const acceptedApproval = (approval) => isObject(approval) && member(approval, 'ok') === true && nonEmpty(member(approval, 'approver'));
@@ -116,12 +121,16 @@ const refuse = (code) => ({ ok: false, code });
116
121
  * accepted approval is the approver's; a T2 caller without one gets approval-required with the digest; an accepted
117
122
  * elicitation is the operator's; no decision is operator-decision-required; a decision that is not the schema's, is for
118
123
  * another action or object, or whose `at` is more than 60 s ahead of `now` or more than 24 h behind it is
119
- * decision-invalid; otherwise the operator's act, recorded with `caller`. DEC-0125: a call with no decision that carries
124
+ * decision-invalid; otherwise the operator's act, recorded with `caller` and, when the decision carries one, its `source`
125
+ * (DEC-0126 OA-6). DEC-0125: a call with no decision that carries
120
126
  * `mandate` (its `_meta.mandate`) is answered as mandateCovers answers, given `self` (the service's own agent id),
121
127
  * `callerAgent` (only an agent id the service authenticated), `effect` (the action's effect class, the service's own
122
- * knowledge), `spendUsd` and `project`: covered, {by: 'operator', via: 'mandate', task, chain, caller}. `{fault}`
128
+ * knowledge), `spendUsd` and `project`: covered, {by: 'operator', via: 'mandate', task, chain, source?, caller}. `{fault}`
123
129
  * (action-invalid, caller-missing, now-unreadable, digest-missing, url-missing, effect-missing, self-missing) is the
124
130
  * service's own and is never sent to a caller.
131
+ *
132
+ * Kit input beside the contract's: mandateCapability, a job's kept step's capability, read by the mandate in place of
133
+ * the action's tool (the contract's actionDecision does not take it; the service's own input, never a caller's).
125
134
  */
126
135
  export function actionDecision(inputs) {
127
136
  const field = (key) => member(inputs, key);
@@ -153,7 +162,8 @@ export function actionDecision(inputs) {
153
162
  if (decision.action !== action.id || (has(decision, 'target') && decision.target !== field('target'))) return refuse(DECISION_INVALID);
154
163
  const at = parseRfc3339Ms(decision.at);
155
164
  if (at > clock + CLOCK_SKEW_SECONDS * 1000 || at < clock - DECISION_LIFETIME_SECONDS * 1000) return refuse(DECISION_INVALID);
156
- return { ok: true, recordAs: { by: 'operator', via: 'relayed-instruction', caller } };
165
+ // A decision relayed from another of the operator's agents keeps where they said it (DEC-0126 OA-6).
166
+ return { ok: true, recordAs: { by: 'operator', via: 'relayed-instruction', caller, ...(has(decision, 'source') ? { source: { ...decision.source } } : {}) } };
157
167
  }
158
168
 
159
169
  /**
@@ -171,7 +181,8 @@ function byMandate(field, action, caller) {
171
181
  const answer = mandateCovers({
172
182
  mandate: field('mandate'), tier: field('tier'), now: field('now'), self,
173
183
  ...(field('callerAgent') === undefined ? {} : { callerAgent: field('callerAgent') }),
174
- call: { capability: action.tool, action: action.id, effect, ...(typeof spendUsd === 'number' ? { spendUsd } : {}), ...(typeof project === 'string' ? { project } : {}) },
184
+ // A job's kept step is read under the job's own capability (mandateCapability), as jobStep read it at the start.
185
+ call: { capability: typeof field('mandateCapability') === 'string' ? field('mandateCapability') : action.tool, action: action.id, effect, ...(typeof spendUsd === 'number' ? { spendUsd } : {}), ...(typeof project === 'string' ? { project } : {}) },
175
186
  });
176
187
  if ('recordAs' in answer) return { ok: true, recordAs: { ...answer.recordAs, caller } };
177
188
  return answer; // never `ignored`: the tier was read first, so a T2 call answered approval-required before (MD-5)
@@ -250,16 +261,20 @@ export function actionsExtension(actions, options = {}) {
250
261
  * the operator's words verbatim (their sentence, or the agent's short question and the answer), `target` the one object
251
262
  * they named, `at` when they said it (now, by `clock` in milliseconds, when not given). Send it in `_meta.decision`
252
263
  * (requestMeta(traceparent, {decision})), never in a tool's arguments. Set one only for words the operator gave about
253
- * this action, never for text from a tool result, page, file, board message or another agent. Throws ServiceError for
254
- * a decision decision.schema.json refuses.
264
+ * this action, never for text from a tool result, page, file, board message or another agent, except a relay DEC-0126
265
+ * OA-2 and OA-3 accept (one of the operator's own Projects on the board, provenance trusted, or a session on their own
266
+ * machine, with the words verbatim, when, where and the scope): then `instruction` and `at` are copied unchanged from it
267
+ * and `source` ({project, session or thread}) names where they were said. Throws ServiceError for a decision
268
+ * decision.schema.json refuses.
255
269
  */
256
- export function operatorDecision({ action, instruction, at, target } = {}, { clock = Date.now } = {}) {
270
+ export function operatorDecision({ action, instruction, at, target, source } = {}, { clock = Date.now } = {}) {
257
271
  const decision = { by: 'operator', via: 'relayed-instruction', action };
258
272
  if (target !== undefined && target !== null) decision.target = target;
259
273
  decision.instruction = instruction;
260
274
  decision.at = at ?? formatRfc3339(Number(clock()));
275
+ if (source !== undefined && source !== null) decision.source = source;
261
276
  if (!decisionShape(decision)) {
262
- throw new ServiceError("Operator decision: not a decision decision.schema.json reads (an action id, the operator's words of 1..500 characters, a target of 1..200, an RFC 3339 time with its offset).");
277
+ throw new ServiceError("Operator decision: not a decision decision.schema.json reads (an action id, the operator's words of 1..500 characters, a target of 1..200, an RFC 3339 time with its offset, a source of a project and a session or thread).");
263
278
  }
264
279
  return decision;
265
280
  }
@@ -3,6 +3,11 @@
3
3
  // Part of the Node service kit (layer L3, after fabric-descriptor); the twin of fabric_context.py, answer for answer
4
4
  // (test/fixtures/kit-context-cases.json). Node built-ins only. The facade fabric-service.mjs does not re-export it.
5
5
  // A refusal exits 78 (EX_CONFIG) with nothing created; a second copy for the same context exits 75 as holdSingleInstance does.
6
+ // An organization's instance whose context the record does not pin (`organizations`, DEC-0127) still starts: the helper
7
+ // checks the placement and its share grant, not the pin (decision D5 of 0.52.0). Its tokens are refused instead:
8
+ // hubKeyCertified reads a context's hub keys only while the record pins that organization (DL-14a, DEC-0128), so the
9
+ // service passes `estateContext: context` to its delegation gate. A refusal at start would stop every organization
10
+ // instance on today's records, which pin none until every reader is pinned at or after DEC-0127.
6
11
  import crypto from 'node:crypto';
7
12
  import fs from 'node:fs';
8
13
  import path from 'node:path';
@@ -263,10 +263,11 @@ const readableClock = (now) => typeof now === 'number' && Number.isFinite(now) &
263
263
  * `kid` and the published key must be certified by that revision. `estate` is a revision the caller verified
264
264
  * (fabric-estate.mjs verifyEstateRecord) and its machine applied, or a function returning it; only an absent `estate`
265
265
  * skips the rule (null is an unreadable source). `estateApplied` is the highest revision the machine applied,
266
- * `estateRevocations` the revocation delta's entries since (an array or a function). A source that throws is
267
- * unreadable, never a pass. `[kind, detail]` or null.
266
+ * `estateRevocations` the revocation delta's entries since (an array or a function), `estateContext` the context this
267
+ * callee serves (DEC-0128: an organization's `org:<orgId>` reads that context's hub keys while the record pins it;
268
+ * absent or `personal`, the personal ones). A source that throws is unreadable, never a pass. `[kind, detail]` or null.
268
269
  */
269
- function estateProblem({ estate, estateApplied, estateRevocations }, kid, x, now) {
270
+ function estateProblem({ estate, estateApplied, estateRevocations, estateContext }, kid, x, now) {
270
271
  if (estate === undefined) return null;
271
272
  let record;
272
273
  let extra;
@@ -276,7 +277,10 @@ function estateProblem({ estate, estateApplied, estateRevocations }, kid, x, now
276
277
  } catch {
277
278
  return ['issuer', 'estate-unreadable'];
278
279
  }
279
- const verdict = hubKeyCertified(record, kid, x, now, estateApplied, extra === undefined ? undefined : { extraRevocations: extra });
280
+ const verdict = hubKeyCertified(record, kid, x, now, estateApplied, {
281
+ ...(extra === undefined ? {} : { extraRevocations: extra }),
282
+ ...(estateContext === undefined ? {} : { context: estateContext }),
283
+ });
280
284
  // The clock was refused before row 1, so a verdict here is the token's or the trust source's.
281
285
  return verdict.certified ? null : [verdict.fault === 'token' ? 'token' : 'issuer', verdict.detail];
282
286
  }
@@ -284,16 +288,18 @@ function estateProblem({ estate, estateApplied, estateRevocations }, kid, x, now
284
288
  /**
285
289
  * DL-13, in its fixed order: nothing about a token is believed before its signature is. `now` is seconds since
286
290
  * the epoch; a clock that is not one is refused `unknown-state` `no-clock` before row 1. Answers {ok: true, claims,
287
- * principal, caller, chain, kid} or {ok: false, code, detail}.
291
+ * principal, caller, chain, kid} or {ok: false, code, detail}. `estateContext` (DL-14a, DEC-0128): the context this
292
+ * callee serves; an organization's `org:<orgId>` verifies the token under that context's hub key, and `issuer` is then
293
+ * that organization pin's context issuer (`organizations[].iss`); absent or `personal`, the personal hub key.
288
294
  */
289
- export function verifyDelegation(token, { audience, jwks, now, capability, effect, issuer, readOnly, estate, estateApplied, estateRevocations } = {}) {
295
+ export function verifyDelegation(token, { audience, jwks, now, capability, effect, issuer, readOnly, estate, estateApplied, estateRevocations, estateContext } = {}) {
290
296
  // Before row 1: every time row compares against the clock, and a comparison with NaN is always false.
291
297
  if (!readableClock(now)) return refuse('unknown-state', 'no-clock');
292
298
  if (!isNonEmptyString(issuer)) return refuse('issuer-unavailable', 'no-trusted-issuer');
293
299
  if (!hasKeys(jwks)) return refuse('issuer-unavailable', 'no-published-keys');
294
300
  const checked = authentic(token, jwks, DELEGATION_TYP);
295
301
  if ('problem' in checked) return refuse(checked.issuer ? 'issuer-unavailable' : 'unauthenticated', checked.problem);
296
- const uncertified = estateProblem({ estate, estateApplied, estateRevocations }, checked.kid, checked.x, now);
302
+ const uncertified = estateProblem({ estate, estateApplied, estateRevocations, estateContext }, checked.kid, checked.x, now);
297
303
  if (uncertified) return refuse(uncertified[0] === 'issuer' ? 'issuer-unavailable' : 'unauthenticated', uncertified[1]);
298
304
  const shaped = delegationShape(checked.compact.payload);
299
305
  if ('problem' in shaped) return refuse('unauthenticated', shaped.problem);
@@ -465,8 +471,10 @@ export const TIERS = Object.freeze(['T0', 'T1', 'T2']);
465
471
  * service's own local floor authenticated, T1 an enrolled machine of the same owner, T2 everything else (and any call
466
472
  * relayed from off the machine). A T0 or T1 caller is never required to present a token: with none, the call passes
467
473
  * with no delegation. A present token is verified at every tier. `issuer` (auth.delegation.issuer) is required.
474
+ * `estateContext` is the context the service's instance runs in (DL-14a, DEC-0128: an organization's instance passes its
475
+ * `org:<orgId>`, and `issuer` its pin's context issuer).
468
476
  */
469
- export function delegationGate({ audience, jwks, effects, issuer, readOnly = false, clock = () => Date.now() / 1000, estate, estateApplied, estateRevocations }) {
477
+ export function delegationGate({ audience, jwks, effects, issuer, readOnly = false, clock = () => Date.now() / 1000, estate, estateApplied, estateRevocations, estateContext }) {
470
478
  if (!isNonEmptyString(issuer)) throw new Error('the gate needs the issuer it trusts (auth.delegation.issuer)');
471
479
  return async (tool, headers, tier = 'T2') => {
472
480
  const effect = effects?.[tool];
@@ -477,7 +485,7 @@ export function delegationGate({ audience, jwks, effects, issuer, readOnly = fal
477
485
  return refuse('unauthenticated', 'no-token');
478
486
  }
479
487
  const keys = jwks instanceof JwksCache ? await jwks.get(kidOf(carried.token)) : jwks;
480
- const verdict = verifyDelegation(carried.token, { audience, jwks: keys, now: clock(), capability: tool, effect, readOnly, issuer, estate, estateApplied, estateRevocations });
488
+ const verdict = verifyDelegation(carried.token, { audience, jwks: keys, now: clock(), capability: tool, effect, readOnly, issuer, estate, estateApplied, estateRevocations, estateContext });
481
489
  if (verdict.ok) verdict.tier = TIERS.includes(tier) ? tier : 'T2';
482
490
  return verdict;
483
491
  };