@kybernesis/dispatch 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel.d.ts +17 -5
- package/dist/channel.js +28 -7
- package/dist/governed.d.ts +27 -0
- package/dist/governed.js +50 -0
- package/dist/index.d.ts +1 -0
- package/dist/remote-peer.d.ts +31 -10
- package/dist/remote-peer.js +54 -14
- package/package.json +8 -2
package/dist/channel.d.ts
CHANGED
|
@@ -26,14 +26,26 @@ export declare function peerSubject(peer: PeerRef): string;
|
|
|
26
26
|
export interface DispatchChannelOptions {
|
|
27
27
|
/**
|
|
28
28
|
* The deployments allowed to call this agent AND assert a forwarded
|
|
29
|
-
* end-user principal.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* end-user principal. There is deliberately no predicate form: enumerating
|
|
30
|
+
* concrete peers is the whole point. Optional only when `governed` is set;
|
|
31
|
+
* an agent with neither should simply not author a dispatch channel.
|
|
32
32
|
*/
|
|
33
|
-
trustedPeers
|
|
33
|
+
trustedPeers?: readonly PeerRef[];
|
|
34
|
+
/**
|
|
35
|
+
* GOVERNED mode: verify A2A session tokens minted by the Kybernesis control
|
|
36
|
+
* plane for granted caller→callee edges (requires @kybernesis/enterprise
|
|
37
|
+
* ≥0.2.0 installed). `agent` is THIS agent's registered name; a verified
|
|
38
|
+
* a2a caller may call and assert forwarded principals — no peer enumeration
|
|
39
|
+
* needed, and revoking the edge in the admin locks it out within the token
|
|
40
|
+
* TTL (default 300 s). Composes with `trustedPeers` (union).
|
|
41
|
+
*/
|
|
42
|
+
governed?: {
|
|
43
|
+
issuer: string;
|
|
44
|
+
agent: string;
|
|
45
|
+
};
|
|
34
46
|
/**
|
|
35
47
|
* Extra `AuthFn`s appended to the walk (e.g. your web app's auth for a
|
|
36
|
-
* browser frontend). The
|
|
48
|
+
* browser frontend). The peer verifiers always run first.
|
|
37
49
|
*/
|
|
38
50
|
extraAuth?: Parameters<typeof eveChannel>[0] extends {
|
|
39
51
|
auth?: infer A;
|
package/dist/channel.js
CHANGED
|
@@ -35,18 +35,35 @@ export function peerSubject(peer) {
|
|
|
35
35
|
* via `placeholderAuth()` unless you pass real auth in `extraAuth`.
|
|
36
36
|
*/
|
|
37
37
|
export function dispatchChannel(options) {
|
|
38
|
-
const peers = options.trustedPeers;
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const peers = options.trustedPeers ?? [];
|
|
39
|
+
const governed = options.governed;
|
|
40
|
+
if (peers.length === 0 && !governed) {
|
|
41
|
+
throw new Error("dispatchChannel: name at least one trustedPeers entry or set governed — an agent with no peers should not author a dispatch channel.");
|
|
41
42
|
}
|
|
42
43
|
const subjects = peers.map(peerSubject);
|
|
43
44
|
const subjectSet = new Set(subjects);
|
|
44
45
|
const extra = (options.extraAuth ?? []);
|
|
46
|
+
let governedAuth;
|
|
47
|
+
const governedAuthFn = async (request) => {
|
|
48
|
+
if (governedAuth === undefined) {
|
|
49
|
+
try {
|
|
50
|
+
const mod = (await import("@kybernesis/enterprise"));
|
|
51
|
+
governedAuth = mod.kybernesisAuth({ issuer: governed.issuer, agent: governed.agent });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.error("[dispatch] governed mode requires @kybernesis/enterprise >=0.2.0 — install it or remove `governed`.");
|
|
55
|
+
governedAuth = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return governedAuth ? governedAuth(request) : null;
|
|
59
|
+
};
|
|
45
60
|
return eveChannel({
|
|
46
61
|
auth: [
|
|
47
62
|
// Verifies which deployment is calling: this project's own tokens are
|
|
48
63
|
// always accepted; other projects only when enumerated as peers.
|
|
49
|
-
vercelOidc({ subjects }),
|
|
64
|
+
...(peers.length > 0 ? [vercelOidc({ subjects })] : []),
|
|
65
|
+
// Governed callers present a control-plane A2A session token instead.
|
|
66
|
+
...(governed ? [governedAuthFn] : []),
|
|
50
67
|
// Open on localhost for `eve dev` and the REPL; ignored in production.
|
|
51
68
|
localDev(),
|
|
52
69
|
...extra,
|
|
@@ -54,8 +71,12 @@ export function dispatchChannel(options) {
|
|
|
54
71
|
// caller passed real auth above.
|
|
55
72
|
placeholderAuth(),
|
|
56
73
|
],
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
// Enumerated peers OR a verified a2a agent principal (whose edge grant was
|
|
75
|
+
// checked at mint, ≤TTL ago) may assert a forwarded end-user principal.
|
|
76
|
+
trustedForwarders: (forwarder) => (typeof forwarder.subject === "string" && subjectSet.has(forwarder.subject)) ||
|
|
77
|
+
(Boolean(governed) &&
|
|
78
|
+
forwarder.principalType === "agent" &&
|
|
79
|
+
forwarder.authenticator === "kybernesis" &&
|
|
80
|
+
forwarder.attributes?.kind === "a2a"),
|
|
60
81
|
});
|
|
61
82
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governed mode: edges resolved from the Kybernesis control plane instead of
|
|
3
|
+
* hand-enumerated peers. The caller authenticates to the control plane with its
|
|
4
|
+
* agent credential (KYBERNESIS_AGENT_CREDENTIAL env), receives a short-TTL A2A
|
|
5
|
+
* session token for a GRANTED caller→callee edge plus the callee's deployment
|
|
6
|
+
* URL — so the registry doubles as discovery, and revoking the edge in the
|
|
7
|
+
* admin takes effect at the next mint (≤ the token TTL, default 300 s).
|
|
8
|
+
*/
|
|
9
|
+
export interface GovernedOptions {
|
|
10
|
+
/** Control-plane issuer URL (e.g. https://agent.kybernesis.ai). */
|
|
11
|
+
issuer: string;
|
|
12
|
+
}
|
|
13
|
+
interface MintedSession {
|
|
14
|
+
token: string;
|
|
15
|
+
callee: {
|
|
16
|
+
name: string;
|
|
17
|
+
deploymentUrl: string | null;
|
|
18
|
+
};
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Per-edge mint cache. url() and auth() both need the mint result and are
|
|
23
|
+
* invoked independently by eve — a shared in-flight promise keeps them to one
|
|
24
|
+
* control-plane call per TTL window.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createA2ASessionSource(issuer: string, callee: string): () => Promise<MintedSession>;
|
|
27
|
+
export {};
|
package/dist/governed.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governed mode: edges resolved from the Kybernesis control plane instead of
|
|
3
|
+
* hand-enumerated peers. The caller authenticates to the control plane with its
|
|
4
|
+
* agent credential (KYBERNESIS_AGENT_CREDENTIAL env), receives a short-TTL A2A
|
|
5
|
+
* session token for a GRANTED caller→callee edge plus the callee's deployment
|
|
6
|
+
* URL — so the registry doubles as discovery, and revoking the edge in the
|
|
7
|
+
* admin takes effect at the next mint (≤ the token TTL, default 300 s).
|
|
8
|
+
*/
|
|
9
|
+
/** Re-mint 30 s before expiry so an in-flight dispatch never carries a stale token. */
|
|
10
|
+
const EXPIRY_BUFFER_SEC = 30;
|
|
11
|
+
/**
|
|
12
|
+
* Per-edge mint cache. url() and auth() both need the mint result and are
|
|
13
|
+
* invoked independently by eve — a shared in-flight promise keeps them to one
|
|
14
|
+
* control-plane call per TTL window.
|
|
15
|
+
*/
|
|
16
|
+
export function createA2ASessionSource(issuer, callee) {
|
|
17
|
+
let cached = null;
|
|
18
|
+
let inflight = null;
|
|
19
|
+
const mint = async () => {
|
|
20
|
+
const credential = process.env.KYBERNESIS_AGENT_CREDENTIAL;
|
|
21
|
+
if (!credential) {
|
|
22
|
+
throw new Error("governed remotePeer: KYBERNESIS_AGENT_CREDENTIAL is not set — mint it in the control plane (agent panel) and add it to this deployment's env.");
|
|
23
|
+
}
|
|
24
|
+
const res = await fetch(`${issuer.replace(/\/$/, "")}/api/agent/session`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
authorization: `Bearer ${credential}`,
|
|
28
|
+
"content-type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ callee }),
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const body = (await res.json().catch(() => ({})));
|
|
34
|
+
throw new Error(`governed remotePeer: A2A mint for callee "${callee}" failed (${res.status} ${body.error ?? "unknown"}) — check the edge grant and both agents' status in the control plane.`);
|
|
35
|
+
}
|
|
36
|
+
return (await res.json());
|
|
37
|
+
};
|
|
38
|
+
return async () => {
|
|
39
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
40
|
+
if (cached && cached.expiresAt - EXPIRY_BUFFER_SEC > nowSec)
|
|
41
|
+
return cached;
|
|
42
|
+
if (!inflight) {
|
|
43
|
+
inflight = mint().finally(() => {
|
|
44
|
+
inflight = null;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
cached = await inflight;
|
|
48
|
+
return cached;
|
|
49
|
+
};
|
|
50
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,4 +22,5 @@
|
|
|
22
22
|
* app's service identity. Upgrade both ends of an edge together.
|
|
23
23
|
*/
|
|
24
24
|
export { remotePeer, type RemotePeerOptions } from "./remote-peer.js";
|
|
25
|
+
export { type GovernedOptions } from "./governed.js";
|
|
25
26
|
export { dispatchChannel, peerSubject, type DispatchChannelOptions, type PeerRef, } from "./channel.js";
|
package/dist/remote-peer.d.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { defineRemoteAgent } from "eve";
|
|
2
|
+
import { type GovernedOptions } from "./governed.js";
|
|
2
3
|
/** Options for {@link remotePeer}. */
|
|
3
4
|
export interface RemotePeerOptions {
|
|
4
5
|
/**
|
|
5
6
|
* Environment variable holding the peer deployment's URL (e.g.
|
|
6
7
|
* `"GTM_AGENT_URL"`). Read at runtime on every dispatch, so repointing the
|
|
7
|
-
* edge is an env change, not a rebuild.
|
|
8
|
+
* edge is an env change, not a rebuild. Required in ungoverned mode; in
|
|
9
|
+
* governed mode it becomes an OVERRIDE on top of registry discovery.
|
|
8
10
|
*/
|
|
9
|
-
envVar
|
|
11
|
+
envVar?: string;
|
|
10
12
|
/**
|
|
11
13
|
* Fallback URL when the env var is unset. Optional — without it, a missing
|
|
12
14
|
* env var fails the dispatch loudly, which is usually what you want in a
|
|
13
15
|
* fresh environment.
|
|
14
16
|
*/
|
|
15
17
|
fallbackUrl?: string;
|
|
18
|
+
/**
|
|
19
|
+
* GOVERNED mode: the callee's registered name in the control plane. The edge
|
|
20
|
+
* must be granted (caller→callee) in the admin; outbound auth becomes a
|
|
21
|
+
* short-TTL A2A session token minted from `governed.issuer` using this
|
|
22
|
+
* deployment's `KYBERNESIS_AGENT_CREDENTIAL`, and the callee's URL is
|
|
23
|
+
* resolved from the registry (env var still wins as an override).
|
|
24
|
+
*/
|
|
25
|
+
callee?: string;
|
|
26
|
+
/** Control-plane settings for governed mode. Requires `callee`. */
|
|
27
|
+
governed?: GovernedOptions;
|
|
16
28
|
/**
|
|
17
29
|
* The routing hint. The calling agent's model reads this to decide when to
|
|
18
30
|
* delegate here — write it like the specialist's job description, with the
|
|
@@ -22,9 +34,9 @@ export interface RemotePeerOptions {
|
|
|
22
34
|
description: string;
|
|
23
35
|
/**
|
|
24
36
|
* Forward the verified end-user principal across the hop (default `true`).
|
|
25
|
-
* The receiver must
|
|
26
|
-
*
|
|
27
|
-
* fails with a 403 rather than silently downgrading.
|
|
37
|
+
* The receiver must trust this deployment (trustedPeers, or the verified
|
|
38
|
+
* a2a caller principal in governed mode) for the assertion to be accepted —
|
|
39
|
+
* otherwise the dispatch fails with a 403 rather than silently downgrading.
|
|
28
40
|
*
|
|
29
41
|
* Set `false` only when the peer should treat calls as coming from this
|
|
30
42
|
* app itself (service identity) rather than from the human behind it.
|
|
@@ -44,17 +56,26 @@ export interface RemotePeerOptions {
|
|
|
44
56
|
* subagent, with eve's durable dispatch underneath: the calling turn parks
|
|
45
57
|
* without holding compute until the peer posts its terminal callback.
|
|
46
58
|
*
|
|
59
|
+
* Ungoverned (hand-wired):
|
|
47
60
|
* ```ts title="agent/subagents/gtm.ts"
|
|
48
61
|
* import { remotePeer } from "@kybernesis/dispatch";
|
|
49
|
-
*
|
|
50
62
|
* export default remotePeer({
|
|
51
63
|
* envVar: "GTM_AGENT_URL",
|
|
52
|
-
* description:
|
|
53
|
-
* "The company's GTM operator: posting cadence, open plays, outreach targets, content drafting.",
|
|
64
|
+
* description: "The company's GTM operator: cadence, plays, targets, drafting.",
|
|
54
65
|
* });
|
|
55
66
|
* ```
|
|
67
|
+
* Outbound auth is Vercel OIDC — the peer verifies which deployment is calling.
|
|
56
68
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
69
|
+
* Governed (control-plane edges):
|
|
70
|
+
* ```ts title="agent/subagents/gtm.ts"
|
|
71
|
+
* export default remotePeer({
|
|
72
|
+
* callee: "eve-gtm",
|
|
73
|
+
* governed: { issuer: "https://agent.kybernesis.ai" },
|
|
74
|
+
* description: "The company's GTM operator: cadence, plays, targets, drafting.",
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
* Outbound auth is a 5-minute A2A token minted per granted edge; the callee's
|
|
78
|
+
* URL comes from the registry. Revoke the edge in the admin → the next mint
|
|
79
|
+
* refuses → dispatches 403 within the token TTL.
|
|
59
80
|
*/
|
|
60
81
|
export declare function remotePeer(options: RemotePeerOptions): import("eve").RemoteAgentDefinition;
|
package/dist/remote-peer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineRemoteAgent } from "eve";
|
|
2
2
|
import { vercelOidc } from "eve/agents/auth";
|
|
3
|
+
import { createA2ASessionSource } from "./governed.js";
|
|
3
4
|
/**
|
|
4
5
|
* Declares a separately deployed eve agent as a callable peer of this agent.
|
|
5
6
|
*
|
|
@@ -8,31 +9,70 @@ import { vercelOidc } from "eve/agents/auth";
|
|
|
8
9
|
* subagent, with eve's durable dispatch underneath: the calling turn parks
|
|
9
10
|
* without holding compute until the peer posts its terminal callback.
|
|
10
11
|
*
|
|
12
|
+
* Ungoverned (hand-wired):
|
|
11
13
|
* ```ts title="agent/subagents/gtm.ts"
|
|
12
14
|
* import { remotePeer } from "@kybernesis/dispatch";
|
|
13
|
-
*
|
|
14
15
|
* export default remotePeer({
|
|
15
16
|
* envVar: "GTM_AGENT_URL",
|
|
16
|
-
* description:
|
|
17
|
-
* "The company's GTM operator: posting cadence, open plays, outreach targets, content drafting.",
|
|
17
|
+
* description: "The company's GTM operator: cadence, plays, targets, drafting.",
|
|
18
18
|
* });
|
|
19
19
|
* ```
|
|
20
|
+
* Outbound auth is Vercel OIDC — the peer verifies which deployment is calling.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* Governed (control-plane edges):
|
|
23
|
+
* ```ts title="agent/subagents/gtm.ts"
|
|
24
|
+
* export default remotePeer({
|
|
25
|
+
* callee: "eve-gtm",
|
|
26
|
+
* governed: { issuer: "https://agent.kybernesis.ai" },
|
|
27
|
+
* description: "The company's GTM operator: cadence, plays, targets, drafting.",
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
* Outbound auth is a 5-minute A2A token minted per granted edge; the callee's
|
|
31
|
+
* URL comes from the registry. Revoke the edge in the admin → the next mint
|
|
32
|
+
* refuses → dispatches 403 within the token TTL.
|
|
23
33
|
*/
|
|
24
34
|
export function remotePeer(options) {
|
|
25
|
-
const { envVar, fallbackUrl, description, outputSchema } = options;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const { envVar, fallbackUrl, description, outputSchema, callee, governed } = options;
|
|
36
|
+
if (governed && !callee) {
|
|
37
|
+
throw new Error("remotePeer: governed mode requires `callee` (the peer's registered control-plane name).");
|
|
38
|
+
}
|
|
39
|
+
const session = governed && callee ? createA2ASessionSource(governed.issuer, callee) : null;
|
|
40
|
+
// eve resolves this at agent-graph build (boot), not per dispatch — so URL
|
|
41
|
+
// resolution must DEGRADE, never throw for a merely-missing credential:
|
|
42
|
+
// a misconfigured edge should fail when dispatched, not prevent the whole
|
|
43
|
+
// agent (and its evals) from booting.
|
|
44
|
+
const url = async () => {
|
|
45
|
+
const override = envVar ? process.env[envVar] : undefined;
|
|
46
|
+
if (override)
|
|
47
|
+
return override;
|
|
48
|
+
if (session && process.env.KYBERNESIS_AGENT_CREDENTIAL) {
|
|
49
|
+
try {
|
|
50
|
+
const s = await session();
|
|
51
|
+
if (s.callee.deploymentUrl)
|
|
52
|
+
return s.callee.deploymentUrl;
|
|
31
53
|
}
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
catch {
|
|
55
|
+
// Mint failed (control plane down, edge revoked): fall through to the
|
|
56
|
+
// static fallback so boot survives; the dispatch itself will fail
|
|
57
|
+
// loudly in auth() with the precise mint error.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (fallbackUrl)
|
|
61
|
+
return fallbackUrl;
|
|
62
|
+
throw new Error(`remotePeer: no URL for this edge — ${session
|
|
63
|
+
? "registry discovery unavailable (no credential or no registered deploymentUrl) and no envVar/fallbackUrl override is set."
|
|
64
|
+
: `${envVar ?? "envVar"} is not set and no fallbackUrl was provided.`}`);
|
|
65
|
+
};
|
|
66
|
+
const auth = session
|
|
67
|
+
? async () => {
|
|
68
|
+
const s = await session();
|
|
69
|
+
return { headers: { authorization: `Bearer ${s.token}` } };
|
|
70
|
+
}
|
|
71
|
+
: vercelOidc();
|
|
72
|
+
return defineRemoteAgent({
|
|
73
|
+
url,
|
|
34
74
|
description,
|
|
35
|
-
auth
|
|
75
|
+
auth,
|
|
36
76
|
forwardPrincipal: options.forwardPrincipal ?? true,
|
|
37
77
|
...(outputSchema !== undefined ? { outputSchema } : {}),
|
|
38
78
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/dispatch",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Agent-to-agent dispatch for eve agents: declared remote-peer edges with principal forwarding on by default, and a receiver channel that makes trusting the wrong caller hard to express.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"typescript": "7.0.2"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"eve": ">=0.30.0 <0.31.0"
|
|
38
|
+
"eve": ">=0.30.0 <0.31.0",
|
|
39
|
+
"@kybernesis/enterprise": ">=0.2.0"
|
|
39
40
|
},
|
|
40
41
|
"engines": {
|
|
41
42
|
"node": ">=20"
|
|
@@ -47,5 +48,10 @@
|
|
|
47
48
|
},
|
|
48
49
|
"publishConfig": {
|
|
49
50
|
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@kybernesis/enterprise": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
}
|