apcore-a2a 0.5.0 → 0.6.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/README.md +1 -1
- package/dist/adapters/card-visibility.d.ts +148 -0
- package/dist/adapters/card-visibility.d.ts.map +1 -0
- package/dist/adapters/card-visibility.js +237 -0
- package/dist/adapters/card-visibility.js.map +1 -0
- package/dist/adapters/errors.d.ts +39 -4
- package/dist/adapters/errors.d.ts.map +1 -1
- package/dist/adapters/errors.js +89 -7
- package/dist/adapters/errors.js.map +1 -1
- package/dist/adapters/skill-mapper.d.ts +27 -0
- package/dist/adapters/skill-mapper.d.ts.map +1 -1
- package/dist/adapters/skill-mapper.js +61 -0
- package/dist/adapters/skill-mapper.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +16 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/exceptions.d.ts +31 -0
- package/dist/client/exceptions.d.ts.map +1 -1
- package/dist/client/exceptions.js +40 -0
- package/dist/client/exceptions.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/serve.d.ts +12 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +1 -0
- package/dist/serve.js.map +1 -1
- package/dist/server/context.d.ts +12 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +14 -0
- package/dist/server/context.js.map +1 -1
- package/dist/server/executor.d.ts +8 -0
- package/dist/server/executor.d.ts.map +1 -1
- package/dist/server/executor.js +41 -9
- package/dist/server/executor.js.map +1 -1
- package/dist/server/factory.d.ts +12 -0
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +123 -13
- package/dist/server/factory.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -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
|
|
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":"
|
|
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"}
|
package/dist/adapters/errors.js
CHANGED
|
@@ -2,8 +2,57 @@ import { ErrorCodes } from "apcore-js";
|
|
|
2
2
|
const CODE_METHOD_NOT_FOUND = -32601;
|
|
3
3
|
const CODE_INVALID_PARAMS = -32602;
|
|
4
4
|
const CODE_INTERNAL_ERROR = -32603;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A2A 1.0 `TaskNotFoundError`. Reserved for an unknown task id or one owned by
|
|
7
|
+
* another principal — deliberately indistinguishable from each other, and no
|
|
8
|
+
* longer produced for an authorization refusal (see {@link CODE_ACCESS_DENIED}).
|
|
9
|
+
*/
|
|
10
|
+
export const CODE_TASK_NOT_FOUND = -32001;
|
|
11
|
+
// Governance refusal codes (srs FR-ERR-003 / FR-ERR-009 / FR-ERR-010).
|
|
12
|
+
//
|
|
13
|
+
// A2A 1.0 reserves -32001..-32009; JSON-RPC 2.0 leaves -32000..-32099 to the
|
|
14
|
+
// implementation. These three sit above A2A's reserved block, with room for it
|
|
15
|
+
// to grow, and are the "JSON-RPC custom error" A2A §13.2 names as the example
|
|
16
|
+
// for this binding.
|
|
17
|
+
//
|
|
18
|
+
// apcore distinguishes these three refusals from each other and from every
|
|
19
|
+
// other failure. Collapsing them onto -32001 (which means "unknown or non-owned
|
|
20
|
+
// task id") or -32603 (which every agent reads as "retry me") told the caller a
|
|
21
|
+
// *different* failure had happened, one whose correct response is the opposite
|
|
22
|
+
// of the real one.
|
|
23
|
+
export const CODE_ACCESS_DENIED = -32040;
|
|
24
|
+
export const CODE_APPROVAL_DENIED = -32041;
|
|
25
|
+
export const CODE_APPROVAL_TIMEOUT = -32042;
|
|
26
|
+
/**
|
|
27
|
+
* The three governance refusal codes, each with its JSON-RPC code and the fixed
|
|
28
|
+
* message it reports by default.
|
|
29
|
+
*
|
|
30
|
+
* `APPROVAL_PENDING` is deliberately absent: it is a resumable pause carrying
|
|
31
|
+
* the `approvalId` the caller resumes with, handled by the executor before it
|
|
32
|
+
* ever reaches the mapper (srs FR-EXE-002). Sweeping it in here would turn that
|
|
33
|
+
* pause into a terminal failure.
|
|
34
|
+
*/
|
|
35
|
+
const GOVERNANCE_REFUSALS = new Map([
|
|
36
|
+
[ErrorCodes.ACL_DENIED, { code: CODE_ACCESS_DENIED, message: "Access denied" }],
|
|
37
|
+
[ErrorCodes.APPROVAL_DENIED, { code: CODE_APPROVAL_DENIED, message: "Approval denied" }],
|
|
38
|
+
[ErrorCodes.APPROVAL_TIMEOUT, { code: CODE_APPROVAL_TIMEOUT, message: "Approval timed out" }],
|
|
39
|
+
]);
|
|
40
|
+
/** Whether an apcore error code is one of the three governance refusals. */
|
|
41
|
+
export function isGovernanceRefusal(code) {
|
|
42
|
+
return code !== undefined && GOVERNANCE_REFUSALS.has(code);
|
|
43
|
+
}
|
|
6
44
|
export class ErrorMapper {
|
|
45
|
+
discloseRefusalReason;
|
|
46
|
+
/**
|
|
47
|
+
* @param discloseRefusalReason Forward apcore's own message for the three
|
|
48
|
+
* governance refusal codes instead of the fixed per-class string
|
|
49
|
+
* (srs FR-ERR-011). Off by default. The code never changes with the flag —
|
|
50
|
+
* what a refusal *is* does not depend on how much a deployment chooses to
|
|
51
|
+
* say about it.
|
|
52
|
+
*/
|
|
53
|
+
constructor(discloseRefusalReason = false) {
|
|
54
|
+
this.discloseRefusalReason = discloseRefusalReason;
|
|
55
|
+
}
|
|
7
56
|
/** ErrorFormatter interface for apcore ErrorFormatterRegistry. */
|
|
8
57
|
format(error, _context) {
|
|
9
58
|
const rpcError = this.toJsonRpcError(error);
|
|
@@ -25,6 +74,21 @@ export class ErrorMapper {
|
|
|
25
74
|
}
|
|
26
75
|
return { code: CODE_INTERNAL_ERROR, message: "Internal server error" };
|
|
27
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Caller-facing message for a governance refusal.
|
|
79
|
+
*
|
|
80
|
+
* Default: the fixed per-class string. With `discloseRefusalReason`
|
|
81
|
+
* (srs FR-ERR-011): apcore's own message, through the same sanitizer every
|
|
82
|
+
* other forwarded message goes through. An empty or whitespace-only apcore
|
|
83
|
+
* message falls back to the fixed string rather than sending the caller
|
|
84
|
+
* nothing.
|
|
85
|
+
*/
|
|
86
|
+
refusalMessage(fixed, error) {
|
|
87
|
+
if (!this.discloseRefusalReason)
|
|
88
|
+
return fixed;
|
|
89
|
+
const disclosed = this.sanitizeMessage(String(error.message ?? ""));
|
|
90
|
+
return disclosed.trim() ? disclosed : fixed;
|
|
91
|
+
}
|
|
28
92
|
handleApcoreError(error, errorCode) {
|
|
29
93
|
if (errorCode === ErrorCodes.MODULE_NOT_FOUND) {
|
|
30
94
|
const message = this.sanitizeMessage(error.message);
|
|
@@ -43,8 +107,15 @@ export class ErrorMapper {
|
|
|
43
107
|
const description = this.sanitizeMessage(error.message);
|
|
44
108
|
return { code: CODE_INVALID_PARAMS, message: `Invalid input: ${description}` };
|
|
45
109
|
}
|
|
46
|
-
|
|
47
|
-
|
|
110
|
+
// The A2A spec §13.2 MUST NOT forbids revealing *the existence of a
|
|
111
|
+
// resource*, not the *class* of failure. A fixed "Access denied" /
|
|
112
|
+
// "Approval denied" / "Approval timed out" names no caller, target,
|
|
113
|
+
// approver or rule, so it discloses nothing — a caller that named a skill
|
|
114
|
+
// already held that id — while still telling an agent to stop rather than
|
|
115
|
+
// retry.
|
|
116
|
+
const refusal = GOVERNANCE_REFUSALS.get(errorCode);
|
|
117
|
+
if (refusal !== undefined) {
|
|
118
|
+
return { code: refusal.code, message: this.refusalMessage(refusal.message, error) };
|
|
48
119
|
}
|
|
49
120
|
if (errorCode === ErrorCodes.MODULE_TIMEOUT) {
|
|
50
121
|
return { code: CODE_INTERNAL_ERROR, message: "Execution timeout" };
|
|
@@ -119,13 +190,18 @@ export function isServerSideSchemaError(message) {
|
|
|
119
190
|
* {@link sanitizeMessage} does not strip (module ids, versions, env-var names,
|
|
120
191
|
* hostnames). `userFixable` is also settable per-error by the module author,
|
|
121
192
|
* which would let any module widen any fixed per-class string at will,
|
|
122
|
-
* including the
|
|
193
|
+
* including the governance refusals.
|
|
194
|
+
*
|
|
195
|
+
* The three governance codes (`ACL_DENIED`, `APPROVAL_DENIED`,
|
|
196
|
+
* `APPROVAL_TIMEOUT`) are in this partition only when `discloseRefusalReason` is
|
|
197
|
+
* set — the same flag the mapper branches on, so the two surfaces agree under
|
|
198
|
+
* either setting.
|
|
123
199
|
*
|
|
124
200
|
* `errorMapper message policy matches toJsonRpcError` locks this to the
|
|
125
|
-
* branching in `ErrorMapper.handleApcoreError` across every apcore error code
|
|
126
|
-
* so the two cannot drift.
|
|
201
|
+
* branching in `ErrorMapper.handleApcoreError` across every apcore error code
|
|
202
|
+
* and both flag values, so the two cannot drift.
|
|
127
203
|
*/
|
|
128
|
-
export function carriesCallerDetail(error) {
|
|
204
|
+
export function carriesCallerDetail(error, discloseRefusalReason = false) {
|
|
129
205
|
const code = error?.code;
|
|
130
206
|
if (code === ErrorCodes.MODULE_NOT_FOUND || code === ErrorCodes.GENERAL_INVALID_INPUT) {
|
|
131
207
|
return true;
|
|
@@ -133,6 +209,12 @@ export function carriesCallerDetail(error) {
|
|
|
133
209
|
if (code === ErrorCodes.SCHEMA_VALIDATION_ERROR) {
|
|
134
210
|
return !isServerSideSchemaError(String(error.message ?? ""));
|
|
135
211
|
}
|
|
212
|
+
// The three governance codes move into and out of this partition with the
|
|
213
|
+
// flag, so the task-status surface forwards exactly what the JSON-RPC surface
|
|
214
|
+
// does under either setting (srs FR-ERR-011 criterion 4).
|
|
215
|
+
if (isGovernanceRefusal(code)) {
|
|
216
|
+
return discloseRefusalReason;
|
|
217
|
+
}
|
|
136
218
|
return false;
|
|
137
219
|
}
|
|
138
220
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/adapters/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,qBAAqB,GAAG,CAAC,KAAK,CAAC;AACrC,MAAM,mBAAmB,GAAG,CAAC,KAAK,CAAC;AACnC,MAAM,mBAAmB,GAAG,CAAC,KAAK,CAAC;AACnC,MAAM,mBAAmB,GAAG,CAAC,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/adapters/errors.ts"],"names":[],"mappings":"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"}
|