apcore-a2a 0.5.0 → 0.7.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 (57) hide show
  1. package/README.md +72 -1
  2. package/dist/adapters/card-visibility.d.ts +148 -0
  3. package/dist/adapters/card-visibility.d.ts.map +1 -0
  4. package/dist/adapters/card-visibility.js +237 -0
  5. package/dist/adapters/card-visibility.js.map +1 -0
  6. package/dist/adapters/errors.d.ts +39 -4
  7. package/dist/adapters/errors.d.ts.map +1 -1
  8. package/dist/adapters/errors.js +89 -7
  9. package/dist/adapters/errors.js.map +1 -1
  10. package/dist/adapters/skill-mapper.d.ts +27 -0
  11. package/dist/adapters/skill-mapper.d.ts.map +1 -1
  12. package/dist/adapters/skill-mapper.js +61 -0
  13. package/dist/adapters/skill-mapper.js.map +1 -1
  14. package/dist/cli.d.ts +63 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +279 -38
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client/client.d.ts.map +1 -1
  19. package/dist/client/client.js +16 -1
  20. package/dist/client/client.js.map +1 -1
  21. package/dist/client/exceptions.d.ts +31 -0
  22. package/dist/client/exceptions.d.ts.map +1 -1
  23. package/dist/client/exceptions.js +40 -0
  24. package/dist/client/exceptions.js.map +1 -1
  25. package/dist/client/index.d.ts +1 -1
  26. package/dist/client/index.d.ts.map +1 -1
  27. package/dist/client/index.js +1 -1
  28. package/dist/client/index.js.map +1 -1
  29. package/dist/config.d.ts +38 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +71 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/index.d.ts +4 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/openapi-backend.d.ts +203 -0
  38. package/dist/openapi-backend.d.ts.map +1 -0
  39. package/dist/openapi-backend.js +453 -0
  40. package/dist/openapi-backend.js.map +1 -0
  41. package/dist/serve.d.ts +12 -0
  42. package/dist/serve.d.ts.map +1 -1
  43. package/dist/serve.js +1 -0
  44. package/dist/serve.js.map +1 -1
  45. package/dist/server/context.d.ts +12 -1
  46. package/dist/server/context.d.ts.map +1 -1
  47. package/dist/server/context.js +14 -0
  48. package/dist/server/context.js.map +1 -1
  49. package/dist/server/executor.d.ts +8 -0
  50. package/dist/server/executor.d.ts.map +1 -1
  51. package/dist/server/executor.js +41 -9
  52. package/dist/server/executor.js.map +1 -1
  53. package/dist/server/factory.d.ts +12 -0
  54. package/dist/server/factory.d.ts.map +1 -1
  55. package/dist/server/factory.js +133 -25
  56. package/dist/server/factory.js.map +1 -1
  57. package/package.json +4 -3
package/README.md CHANGED
@@ -32,6 +32,7 @@ Built on [`@a2a-js/sdk`](https://www.npmjs.com/package/@a2a-js/sdk) and [Express
32
32
  - **JWT authentication** — tokens bridged to apcore's Identity context
33
33
  - **A2A Explorer UI** — browser UI for discovering and testing skills, with auth bar and cURL generation
34
34
  - **Built-in client** — `A2AClient` for calling remote A2A agents
35
+ - **OpenAPI backend** — point the adapter at an OpenAPI 3.0/3.1 document and every operation becomes an A2A Skill, proxied over HTTP, with no apcore project on the other end
35
36
  - **CLI support** — `npx apcore-a2a serve` for zero-code startup
36
37
  - **Pluggable storage** — swap in Redis or PostgreSQL via the `TaskStore` interface
37
38
  - **Observability** — `/health`, `/metrics` endpoints
@@ -40,7 +41,15 @@ Built on [`@a2a-js/sdk`](https://www.npmjs.com/package/@a2a-js/sdk) and [Express
40
41
  ## Requirements
41
42
 
42
43
  - Node.js >= 18.0.0
43
- - `apcore-js` >= 0.22.0
44
+ - `apcore-js` >= 0.30.0
45
+ - `apcore-toolkit` >= 0.11.1
46
+
47
+ > **OpenAPI backend.** No extra install. The backend needs apcore-toolkit's
48
+ > `OpenAPIScanner`, `loadSpec` and `HTTPProxyRegistryWriter`, all of which ship in
49
+ > the `apcore-toolkit` dependency above and use the global `fetch` available on
50
+ > Node 18+ — so this package declares no `openapi` optional dependency. (The
51
+ > Python distribution declares an `apcore-a2a[openapi]` extra because there the
52
+ > same three pieces sit behind `apcore-toolkit[http-proxy]`.)
44
53
 
45
54
  ---
46
55
 
@@ -80,6 +89,68 @@ npx apcore-a2a serve --extensions-dir ./extensions --port 3000 --explorer --metr
80
89
  npx apcore-a2a serve --extensions-dir ./extensions --auth-type bearer --auth-key mysecret
81
90
  ```
82
91
 
92
+ ### Serve an OpenAPI document as A2A Skills
93
+
94
+ Point the adapter at an OpenAPI 3.0/3.1 document and every operation becomes a
95
+ skill, proxied over HTTP to the API that published it. No apcore project required
96
+ on the other end.
97
+
98
+ ```bash
99
+ npx apcore-a2a serve \
100
+ --from-openapi https://petstore3.swagger.io/api/v3/openapi.json \
101
+ --openapi-prefix petstore \
102
+ --openapi-header "X-Api-Key: $PETSTORE_SPEC_KEY" \
103
+ --openapi-no-deprecated
104
+ ```
105
+
106
+ ```typescript
107
+ import { openapiBackend, serve } from "apcore-a2a";
108
+
109
+ const registry = await openapiBackend("https://api.example.com/openapi.json", {
110
+ prefix: "petstore", // prepended to every derived module ID
111
+ baseUrl: "https://api.example.com", // defaults to servers[0].url
112
+ include: "pets\\..*", // scanner filters
113
+ timeout: 30, // SPEC-FETCH timeout, in SECONDS
114
+ headers: { "X-Api-Key": process.env.PETSTORE_SPEC_KEY! }, // spec fetch only
115
+ authHeaderFactory: () => ({ Authorization: `Bearer ${mintToken()}` }), // proxied calls
116
+ });
117
+
118
+ serve(registry);
119
+ ```
120
+
121
+ Or configure it entirely through the Config Bus:
122
+
123
+ ```yaml
124
+ apcore-a2a:
125
+ openapi:
126
+ spec: "https://api.example.com/openapi.json" # URL, or a path resolved against Config.projectRoot
127
+ base_url: "https://api.example.com"
128
+ prefix: petstore
129
+ include: "pets.*"
130
+ exclude: "*.internal.*"
131
+ include_deprecated: false
132
+ acknowledge_unapproved_writes: false
133
+ timeout: 30.0 # spec fetch, seconds — never the per-call proxy timeout
134
+ headers:
135
+ X-Api-Key: "${PETSTORE_SPEC_KEY}"
136
+ ```
137
+
138
+ Three things are worth knowing before pointing this at a production API:
139
+
140
+ - **Write operations reach the PUBLIC Agent Card.** apcore-toolkit infers
141
+ annotations from the HTTP method and never infers `requiresApproval`, so a
142
+ `POST /charges` is annotated exactly like a `POST /echo` and appears on
143
+ `/.well-known/agent-card.json`, a route that is auth-exempt by design. The
144
+ backend logs a warning naming that exposure; close it with an ACL rule carrying
145
+ `approval: required` (which also withholds the skill from the public card), or
146
+ record the decision with `acknowledge_unapproved_writes: true`.
147
+ - **`headers` authenticate the spec fetch only.** They are never forwarded to
148
+ proxied calls: a document is often public while the API behind it is not.
149
+ Per-request credentials belong in `authHeaderFactory`.
150
+ - **ACL targets move when the upstream renames an operation.** Set `prefix` and
151
+ write the catch-all deny against the prefix (`targets: ["petstore.*"]`), not
152
+ against operation names.
153
+
83
154
  ### Call a remote A2A Agent
84
155
 
85
156
  Use the built-in client to discover and invoke any A2A-compliant agent:
@@ -0,0 +1,148 @@
1
+ import type { AgentCard } from "@a2a-js/sdk";
2
+ import { type Identity } from "apcore-js";
3
+ import type { ModuleDescriptor } from "./skill-mapper.js";
4
+ /**
5
+ * Agent Card skill visibility — who gets to see which skills.
6
+ *
7
+ * apcore's ACL is the authority on who may invoke what, and the discovery
8
+ * surface reflects that authority rather than ignoring it. Two surfaces, two
9
+ * answers:
10
+ *
11
+ * - the **public** card (srs FR-AGC-003) answers "what may *anyone* call": every
12
+ * registered skill, minus apcore's reserved `system.*` management namespace,
13
+ * minus those the ACL denies to the anonymous principal, minus those gated
14
+ * behind a human. It resolves exactly one identity, so it is computed once when
15
+ * the card is built. A per-caller filter here
16
+ * would be strictly more accurate and unaffordable: `/.well-known/` is
17
+ * auth-exempt by design, so every anonymous request would drive
18
+ * `skills.length` calls into the consumer's ACL audit sink, each recording a
19
+ * `deny` decision indistinguishable from a real enforcement event, at whatever
20
+ * rate the client chooses.
21
+ *
22
+ * - the **extended** card (srs FR-AGC-004) answers "what may *you* call": the
23
+ * ACL resolved against the authenticated identity, with `requires_approval`
24
+ * skills restored — an approval gate is a prompt the caller can satisfy, not a
25
+ * refusal. Affordable precisely because that endpoint requires credentials.
26
+ *
27
+ * Only the `system.*` subtraction is unconditional. Every other one is
28
+ * governance-shaped, and with no ACL configured they collapse: the ACL predicates
29
+ * are empty and the `requires_approval` annotation covers only
30
+ * `system.control.*`, leaving the six read modules to publish the deployment's
31
+ * module inventory, health and usage to any anonymous caller. `ACL.discover()`
32
+ * yields nothing for a missing root by design, so "no ACL at all" is the default
33
+ * rather than an edge case — which is why the rule that has to hold there keys on
34
+ * apcore's namespace and not on a governance verdict (srs FR-AGC-003 criteria 12
35
+ * and 13).
36
+ *
37
+ * Authorization and approval are two independent results, not one (apcore
38
+ * PROTOCOL_SPEC §6.1.6), and this module reads them apart. `ACL.check` folds
39
+ * them into a boolean that **fails closed** on an approval requirement —
40
+ * correct for a caller about to execute, wrong for a discovery surface, where it
41
+ * would delete a skill from the extended card for the one reason FR-AGC-004 says
42
+ * to keep it. `ACL.checkAccess` carries both axes, so `access` decides
43
+ * visibility and `approvalRequired` decides only whether the public card is the
44
+ * right surface.
45
+ *
46
+ * Before this module, this binding filtered nothing at all: `buildSkills`
47
+ * iterated `registry.list()` and never consulted the ACL, so a module the ACL
48
+ * denied to everyone was still advertised — by id, name, description and full
49
+ * input schema — to any anonymous caller.
50
+ *
51
+ * **Internal module.** The `export` keywords below are module visibility, not a
52
+ * public contract: `package.json` declares only `"."` in `exports`, so a
53
+ * consumer cannot deep-import this file at all, and nothing here appears in
54
+ * `docs/features/public-api.md`. The surface is expected to move —
55
+ * `allowedSkillIds` already changed meaning once (it now reports the
56
+ * authorization axis alone, where it used to fold in the approval gate). Depend
57
+ * on `serve` / `A2AServerFactory` instead.
58
+ */
59
+ /**
60
+ * apcore's reserved namespace for the runtime's own management modules (apcore
61
+ * PROTOCOL_SPEC §6.7) — `system.health.*`, `system.usage.*`, `system.manifest.*`
62
+ * and, under the second opt-in, `system.control.*`. apcore identifies the
63
+ * surface by this prefix itself, in `Executor.governanceState()`, so matching on
64
+ * it conveys apcore's own boundary rather than inventing one.
65
+ */
66
+ export declare const SYSTEM_NAMESPACE = "system.";
67
+ /** Whether `skillId` is one of apcore's management modules. */
68
+ export declare function isSystemSkill(skillId: string): boolean;
69
+ /** The two axes of one ACL decision (apcore PROTOCOL_SPEC §6.8.1). */
70
+ export interface AccessDecisionLike {
71
+ readonly access: "allow" | "deny";
72
+ readonly approvalRequired: boolean;
73
+ }
74
+ /** The minimum surface this module reads off an apcore ACL. */
75
+ export interface AclLike {
76
+ check(callerId: string | null, targetId: string, context?: unknown): boolean;
77
+ checkAccess?(callerId: string | null, targetId: string, context?: unknown): AccessDecisionLike;
78
+ }
79
+ /** The minimum surface this module reads off an apcore Registry. */
80
+ export interface RegistryLike {
81
+ list(): string[];
82
+ getDefinition(moduleId: string): ModuleDescriptor | null | undefined;
83
+ }
84
+ /**
85
+ * The apcore ACL backing `executor`, if one is configured.
86
+ *
87
+ * apcore-js exposes `setAcl` but no getter, so this reads the public property
88
+ * when one appears upstream and falls back to the private field. `null` means
89
+ * "no ACL configured", which is the common case and leaves every card
90
+ * unfiltered.
91
+ */
92
+ export declare function executorAcl(executor: unknown): AclLike | null;
93
+ /**
94
+ * The skills the ACL permits `identity` to invoke, each mapped to whether
95
+ * invoking it needs a human first.
96
+ *
97
+ * With no ACL configured every id is permitted and none is gated, which is what
98
+ * makes this free for the common single-tenant deployment.
99
+ *
100
+ * The ACL is consulted with **no arguments projection**, because a card is
101
+ * discovery and there is no call site yet. An `arguments` condition (§6.1.7) is
102
+ * therefore unevaluable, so a rule carrying one neither denies nor grants — but
103
+ * an `allow` rule's `approval: required` stays *pending* and composes with
104
+ * whatever grants (§6.1.1 rule 5). A skill gated only for some argument shapes
105
+ * thus reports `true` here: at discovery time "this may need approval" is the
106
+ * honest answer, and it is the one that keeps such a skill off the public card.
107
+ *
108
+ * `callerId` is left `null` deliberately. apcore defines it as the *calling
109
+ * module* in a nested call chain, managed by `Context.child`; a top-level
110
+ * inbound request has none, and the ACL maps `null` to `@external`. That is
111
+ * apcore's contract, not a gap — `callers: ["@external"]` is how an operator
112
+ * denies external access, and it has to keep matching an authenticated request
113
+ * or the rule silently stops covering the traffic it was written for. The
114
+ * authenticated principal travels in the context instead, where the
115
+ * `identityTypes` / `roles` conditions see it.
116
+ */
117
+ export declare function skillAccess(executor: unknown, skillIds: readonly string[], identity: Identity | null): Map<string, boolean>;
118
+ /**
119
+ * The subset of `skillIds` the ACL authorizes `identity` to invoke.
120
+ *
121
+ * The authorization axis alone: a skill the ACL allows but gates behind an
122
+ * approval is in this set, because the caller may reach it. Callers that also
123
+ * need the gate read {@link skillAccess}.
124
+ */
125
+ export declare function allowedSkillIds(executor: unknown, skillIds: readonly string[], identity: Identity | null): Set<string>;
126
+ /**
127
+ * The public card: what an unauthenticated caller could actually invoke.
128
+ *
129
+ * `system.*` is removed unconditionally (srs FR-AGC-003 criteria 12 and 13); the
130
+ * remaining subtractions are governance-shaped. See the module docstring for why
131
+ * this is resolved once rather than per caller.
132
+ */
133
+ export declare function buildPublicCard(card: AgentCard, executor: unknown, registry: RegistryLike | null | undefined): AgentCard;
134
+ /**
135
+ * The extended card: what the authenticated caller may invoke.
136
+ *
137
+ * `requires_approval` skills are kept (srs FR-AGC-004 criterion 2), whether the
138
+ * gate comes from the module's annotation or from an ACL rule. Only the
139
+ * authorization axis of the decision filters here — dropping a skill because it
140
+ * needs a human would report a refusal the ACL never issued.
141
+ *
142
+ * `system.*` is kept too (criterion 11), filtered by the ACL like any other
143
+ * skill: the namespace exclusion is a property of the public card, not of the
144
+ * skill, and an authenticated management agent the ACL permits must still be
145
+ * able to discover the surface it is entitled to drive.
146
+ */
147
+ export declare function buildExtendedCard(card: AgentCard, executor: unknown, identity: Identity | null): AgentCard;
148
+ //# sourceMappingURL=card-visibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-visibility.d.ts","sourceRoot":"","sources":["../../src/adapters/card-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,aAAa,CAAC;AACzD,OAAO,EAAW,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED,+DAA+D;AAC/D,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7E,WAAW,CAAC,CACV,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,OAAO,GAChB,kBAAkB,CAAC;CACvB;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,IAAI,IAAI,MAAM,EAAE,CAAC;IACjB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAAC;CACtE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAQ7D;AA6CD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,QAAQ,GAAG,IAAI,GACxB,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAwBtB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,QAAQ,GAAG,IAAI,GACxB,GAAG,CAAC,MAAM,CAAC,CAEb;AAOD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,GACxC,SAAS,CAqBX;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,QAAQ,GAAG,IAAI,GACxB,SAAS,CAGX"}
@@ -0,0 +1,237 @@
1
+ import { Context } from "apcore-js";
2
+ import { requiresApproval } from "./skill-mapper.js";
3
+ /**
4
+ * Agent Card skill visibility — who gets to see which skills.
5
+ *
6
+ * apcore's ACL is the authority on who may invoke what, and the discovery
7
+ * surface reflects that authority rather than ignoring it. Two surfaces, two
8
+ * answers:
9
+ *
10
+ * - the **public** card (srs FR-AGC-003) answers "what may *anyone* call": every
11
+ * registered skill, minus apcore's reserved `system.*` management namespace,
12
+ * minus those the ACL denies to the anonymous principal, minus those gated
13
+ * behind a human. It resolves exactly one identity, so it is computed once when
14
+ * the card is built. A per-caller filter here
15
+ * would be strictly more accurate and unaffordable: `/.well-known/` is
16
+ * auth-exempt by design, so every anonymous request would drive
17
+ * `skills.length` calls into the consumer's ACL audit sink, each recording a
18
+ * `deny` decision indistinguishable from a real enforcement event, at whatever
19
+ * rate the client chooses.
20
+ *
21
+ * - the **extended** card (srs FR-AGC-004) answers "what may *you* call": the
22
+ * ACL resolved against the authenticated identity, with `requires_approval`
23
+ * skills restored — an approval gate is a prompt the caller can satisfy, not a
24
+ * refusal. Affordable precisely because that endpoint requires credentials.
25
+ *
26
+ * Only the `system.*` subtraction is unconditional. Every other one is
27
+ * governance-shaped, and with no ACL configured they collapse: the ACL predicates
28
+ * are empty and the `requires_approval` annotation covers only
29
+ * `system.control.*`, leaving the six read modules to publish the deployment's
30
+ * module inventory, health and usage to any anonymous caller. `ACL.discover()`
31
+ * yields nothing for a missing root by design, so "no ACL at all" is the default
32
+ * rather than an edge case — which is why the rule that has to hold there keys on
33
+ * apcore's namespace and not on a governance verdict (srs FR-AGC-003 criteria 12
34
+ * and 13).
35
+ *
36
+ * Authorization and approval are two independent results, not one (apcore
37
+ * PROTOCOL_SPEC §6.1.6), and this module reads them apart. `ACL.check` folds
38
+ * them into a boolean that **fails closed** on an approval requirement —
39
+ * correct for a caller about to execute, wrong for a discovery surface, where it
40
+ * would delete a skill from the extended card for the one reason FR-AGC-004 says
41
+ * to keep it. `ACL.checkAccess` carries both axes, so `access` decides
42
+ * visibility and `approvalRequired` decides only whether the public card is the
43
+ * right surface.
44
+ *
45
+ * Before this module, this binding filtered nothing at all: `buildSkills`
46
+ * iterated `registry.list()` and never consulted the ACL, so a module the ACL
47
+ * denied to everyone was still advertised — by id, name, description and full
48
+ * input schema — to any anonymous caller.
49
+ *
50
+ * **Internal module.** The `export` keywords below are module visibility, not a
51
+ * public contract: `package.json` declares only `"."` in `exports`, so a
52
+ * consumer cannot deep-import this file at all, and nothing here appears in
53
+ * `docs/features/public-api.md`. The surface is expected to move —
54
+ * `allowedSkillIds` already changed meaning once (it now reports the
55
+ * authorization axis alone, where it used to fold in the approval gate). Depend
56
+ * on `serve` / `A2AServerFactory` instead.
57
+ */
58
+ /**
59
+ * apcore's reserved namespace for the runtime's own management modules (apcore
60
+ * PROTOCOL_SPEC §6.7) — `system.health.*`, `system.usage.*`, `system.manifest.*`
61
+ * and, under the second opt-in, `system.control.*`. apcore identifies the
62
+ * surface by this prefix itself, in `Executor.governanceState()`, so matching on
63
+ * it conveys apcore's own boundary rather than inventing one.
64
+ */
65
+ export const SYSTEM_NAMESPACE = "system.";
66
+ /** Whether `skillId` is one of apcore's management modules. */
67
+ export function isSystemSkill(skillId) {
68
+ return skillId.startsWith(SYSTEM_NAMESPACE);
69
+ }
70
+ /**
71
+ * The apcore ACL backing `executor`, if one is configured.
72
+ *
73
+ * apcore-js exposes `setAcl` but no getter, so this reads the public property
74
+ * when one appears upstream and falls back to the private field. `null` means
75
+ * "no ACL configured", which is the common case and leaves every card
76
+ * unfiltered.
77
+ */
78
+ export function executorAcl(executor) {
79
+ for (const key of ["acl", "_acl"]) {
80
+ const acl = executor?.[key];
81
+ if (acl && typeof acl.check === "function") {
82
+ return acl;
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * An apcore `Context` carrying `identity`, for conditional ACL rules.
89
+ *
90
+ * An ACL rule's `conditions` block (`identityTypes`, `roles`) is evaluated
91
+ * against the context, and the check returns false without one — so a card
92
+ * filtered with no context would hide every skill a conditional rule allows.
93
+ * Building the context the same way the executor does is what keeps the card and
94
+ * the call path agreeing about the same principal.
95
+ */
96
+ function aclContext(identity) {
97
+ try {
98
+ // apcore-js `Context.create` takes positional arguments (identity first),
99
+ // unlike the Python binding's keyword form. Passing an options object here
100
+ // would silently produce a context with no identity, and every conditional
101
+ // rule would then evaluate false — hiding exactly the skills it allows.
102
+ return Context.create(identity);
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
108
+ /**
109
+ * `[authorized, approvalRequired]` for one skill, from apcore's ACL.
110
+ *
111
+ * Reads `checkAccess` (apcore-js >= 0.28.0, PROTOCOL_SPEC §6.8.1), which reports
112
+ * the two axes separately. The `check` fallback exists for an ACL that predates
113
+ * the accessor: there `approval` did not exist as a rule field, so `false` is
114
+ * not a guess but the only value such an ACL can mean — and a boolean that
115
+ * already fails closed degrades this surface toward showing less, never more.
116
+ */
117
+ function decide(acl, callerId, skillId, ctx) {
118
+ if (typeof acl.checkAccess !== "function") {
119
+ return [acl.check(callerId, skillId, ctx), false];
120
+ }
121
+ const decision = acl.checkAccess(callerId, skillId, ctx);
122
+ return [decision.access === "allow", decision.approvalRequired === true];
123
+ }
124
+ /**
125
+ * The skills the ACL permits `identity` to invoke, each mapped to whether
126
+ * invoking it needs a human first.
127
+ *
128
+ * With no ACL configured every id is permitted and none is gated, which is what
129
+ * makes this free for the common single-tenant deployment.
130
+ *
131
+ * The ACL is consulted with **no arguments projection**, because a card is
132
+ * discovery and there is no call site yet. An `arguments` condition (§6.1.7) is
133
+ * therefore unevaluable, so a rule carrying one neither denies nor grants — but
134
+ * an `allow` rule's `approval: required` stays *pending* and composes with
135
+ * whatever grants (§6.1.1 rule 5). A skill gated only for some argument shapes
136
+ * thus reports `true` here: at discovery time "this may need approval" is the
137
+ * honest answer, and it is the one that keeps such a skill off the public card.
138
+ *
139
+ * `callerId` is left `null` deliberately. apcore defines it as the *calling
140
+ * module* in a nested call chain, managed by `Context.child`; a top-level
141
+ * inbound request has none, and the ACL maps `null` to `@external`. That is
142
+ * apcore's contract, not a gap — `callers: ["@external"]` is how an operator
143
+ * denies external access, and it has to keep matching an authenticated request
144
+ * or the rule silently stops covering the traffic it was written for. The
145
+ * authenticated principal travels in the context instead, where the
146
+ * `identityTypes` / `roles` conditions see it.
147
+ */
148
+ export function skillAccess(executor, skillIds, identity) {
149
+ const access = new Map();
150
+ const acl = executorAcl(executor);
151
+ if (acl === null) {
152
+ for (const skillId of skillIds)
153
+ access.set(skillId, false);
154
+ return access;
155
+ }
156
+ const base = aclContext(identity);
157
+ for (const skillId of skillIds) {
158
+ const ctx = base && typeof base.child === "function"
159
+ ? base.child(skillId)
160
+ : undefined;
161
+ const callerId = ctx?.callerId ?? null;
162
+ try {
163
+ const [authorized, approvalRequired] = decide(acl, callerId, skillId, ctx);
164
+ if (authorized)
165
+ access.set(skillId, approvalRequired);
166
+ }
167
+ catch {
168
+ // A broken ACL must fail closed: serving MORE than the policy allows is
169
+ // the one outcome that cannot be walked back.
170
+ console.warn(`ACL check raised for skill ${skillId}; withholding it`);
171
+ }
172
+ }
173
+ return access;
174
+ }
175
+ /**
176
+ * The subset of `skillIds` the ACL authorizes `identity` to invoke.
177
+ *
178
+ * The authorization axis alone: a skill the ACL allows but gates behind an
179
+ * approval is in this set, because the caller may reach it. Callers that also
180
+ * need the gate read {@link skillAccess}.
181
+ */
182
+ export function allowedSkillIds(executor, skillIds, identity) {
183
+ return new Set(skillAccess(executor, skillIds, identity).keys());
184
+ }
185
+ function withSkills(card, keep) {
186
+ const skills = (card.skills ?? []).filter((skill) => keep.has(skill.id));
187
+ return { ...card, skills };
188
+ }
189
+ /**
190
+ * The public card: what an unauthenticated caller could actually invoke.
191
+ *
192
+ * `system.*` is removed unconditionally (srs FR-AGC-003 criteria 12 and 13); the
193
+ * remaining subtractions are governance-shaped. See the module docstring for why
194
+ * this is resolved once rather than per caller.
195
+ */
196
+ export function buildPublicCard(card, executor, registry) {
197
+ // The management namespace goes first and unconditionally: it is the only
198
+ // subtraction that survives a deployment with no ACL, and skipping the ACL for
199
+ // these ids also keeps `system.*` out of the audit trail of a decision whose
200
+ // answer cannot change the outcome.
201
+ const ids = (card.skills ?? [])
202
+ .map((skill) => skill.id)
203
+ .filter((id) => !isSystemSkill(id));
204
+ const access = skillAccess(executor, ids, null);
205
+ // Both sources of an approval gate, unioned as PROTOCOL_SPEC §6.9 composes
206
+ // them: the module's own annotation, and an ACL rule carrying
207
+ // `approval: required` for this principal. Since apcore 0.28.0 the annotation
208
+ // is one source among several, so reading it alone would leave a skill on the
209
+ // public card that an anonymous caller cannot in fact just call.
210
+ const keep = new Set();
211
+ for (const [skillId, approvalRequired] of access) {
212
+ if (approvalRequired)
213
+ continue;
214
+ if (registry && requiresApproval(registry.getDefinition(skillId)))
215
+ continue;
216
+ keep.add(skillId);
217
+ }
218
+ return withSkills(card, keep);
219
+ }
220
+ /**
221
+ * The extended card: what the authenticated caller may invoke.
222
+ *
223
+ * `requires_approval` skills are kept (srs FR-AGC-004 criterion 2), whether the
224
+ * gate comes from the module's annotation or from an ACL rule. Only the
225
+ * authorization axis of the decision filters here — dropping a skill because it
226
+ * needs a human would report a refusal the ACL never issued.
227
+ *
228
+ * `system.*` is kept too (criterion 11), filtered by the ACL like any other
229
+ * skill: the namespace exclusion is a property of the public card, not of the
230
+ * skill, and an authenticated management agent the ACL permits must still be
231
+ * able to discover the surface it is entitled to drive.
232
+ */
233
+ export function buildExtendedCard(card, executor, identity) {
234
+ const ids = (card.skills ?? []).map((skill) => skill.id);
235
+ return withSkills(card, allowedSkillIds(executor, ids, identity));
236
+ }
237
+ //# sourceMappingURL=card-visibility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-visibility.js","sourceRoot":"","sources":["../../src/adapters/card-visibility.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,41 @@
1
+ /**
2
+ * A2A 1.0 `TaskNotFoundError`. Reserved for an unknown task id or one owned by
3
+ * another principal — deliberately indistinguishable from each other, and no
4
+ * longer produced for an authorization refusal (see {@link CODE_ACCESS_DENIED}).
5
+ */
6
+ export declare const CODE_TASK_NOT_FOUND = -32001;
7
+ export declare const CODE_ACCESS_DENIED = -32040;
8
+ export declare const CODE_APPROVAL_DENIED = -32041;
9
+ export declare const CODE_APPROVAL_TIMEOUT = -32042;
10
+ /** Whether an apcore error code is one of the three governance refusals. */
11
+ export declare function isGovernanceRefusal(code: string | undefined): boolean;
1
12
  export interface JsonRpcError {
2
13
  code: number;
3
14
  message: string;
4
15
  }
5
16
  export declare class ErrorMapper {
17
+ readonly discloseRefusalReason: boolean;
18
+ /**
19
+ * @param discloseRefusalReason Forward apcore's own message for the three
20
+ * governance refusal codes instead of the fixed per-class string
21
+ * (srs FR-ERR-011). Off by default. The code never changes with the flag —
22
+ * what a refusal *is* does not depend on how much a deployment chooses to
23
+ * say about it.
24
+ */
25
+ constructor(discloseRefusalReason?: boolean);
6
26
  /** ErrorFormatter interface for apcore ErrorFormatterRegistry. */
7
27
  format(error: unknown, _context?: unknown): Record<string, unknown>;
8
28
  toJsonRpcError(error: unknown): JsonRpcError;
29
+ /**
30
+ * Caller-facing message for a governance refusal.
31
+ *
32
+ * Default: the fixed per-class string. With `discloseRefusalReason`
33
+ * (srs FR-ERR-011): apcore's own message, through the same sanitizer every
34
+ * other forwarded message goes through. An empty or whitespace-only apcore
35
+ * message falls back to the fixed string rather than sending the caller
36
+ * nothing.
37
+ */
38
+ private refusalMessage;
9
39
  private handleApcoreError;
10
40
  private sanitizeMessage;
11
41
  }
@@ -42,13 +72,18 @@ export declare function isServerSideSchemaError(message: string): boolean;
42
72
  * {@link sanitizeMessage} does not strip (module ids, versions, env-var names,
43
73
  * hostnames). `userFixable` is also settable per-error by the module author,
44
74
  * which would let any module widen any fixed per-class string at will,
45
- * including the `ACL_DENIED` mask.
75
+ * including the governance refusals.
76
+ *
77
+ * The three governance codes (`ACL_DENIED`, `APPROVAL_DENIED`,
78
+ * `APPROVAL_TIMEOUT`) are in this partition only when `discloseRefusalReason` is
79
+ * set — the same flag the mapper branches on, so the two surfaces agree under
80
+ * either setting.
46
81
  *
47
82
  * `errorMapper message policy matches toJsonRpcError` locks this to the
48
- * branching in `ErrorMapper.handleApcoreError` across every apcore error code,
49
- * so the two cannot drift.
83
+ * branching in `ErrorMapper.handleApcoreError` across every apcore error code
84
+ * and both flag values, so the two cannot drift.
50
85
  */
51
- export declare function carriesCallerDetail(error: unknown): boolean;
86
+ export declare function carriesCallerDetail(error: unknown, discloseRefusalReason?: boolean): boolean;
52
87
  /**
53
88
  * Strip file paths, traceback lines and excess whitespace from text bound for a
54
89
  * caller, then truncate to 500 characters. Module-level so the task-status
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/adapters/errors.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,WAAW;IACtB,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAKnE,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY;IAqB5C,OAAO,CAAC,iBAAiB;IA4DzB,OAAO,CAAC,eAAe;CAGxB;AAaD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS3D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMvD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/adapters/errors.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAc1C,eAAO,MAAM,kBAAkB,SAAS,CAAC;AACzC,eAAO,MAAM,oBAAoB,SAAS,CAAC;AAC3C,eAAO,MAAM,qBAAqB,SAAS,CAAC;AAiB5C,4EAA4E;AAC5E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAErE;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,WAAW;IAQV,QAAQ,CAAC,qBAAqB,EAAE,OAAO;IAPnD;;;;;;OAMG;gBACkB,qBAAqB,GAAE,OAAe;IAE3D,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAKnE,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY;IAqB5C;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,iBAAiB;IAmEzB,OAAO,CAAC,eAAe;CAGxB;AAaD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,UAAQ,GAAG,OAAO,CAe1F;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMvD"}