@keemakr/operator-sdk 0.2.0 → 0.4.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/package.json +1 -1
- package/src/conformance/index.ts +9 -0
- package/src/index.ts +44 -0
- package/src/manifest.ts +54 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keemakr/operator-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The keemakr connector contract: defineProvider + ConnectionProvider/ConnectionOperation types + OperationContext (governed fetch) + the connector manifest schema, shared by keemakr-core's built-in adapters and external connector repos. One shape, two consumers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/conformance/index.ts
CHANGED
|
@@ -68,6 +68,15 @@ export async function runConformance(
|
|
|
68
68
|
!manifest.auth_kind || provider.authKind === manifest.auth_kind,
|
|
69
69
|
`code '${provider.authKind}' vs manifest '${manifest.auth_kind ?? '—'}'`,
|
|
70
70
|
);
|
|
71
|
+
// The credential alias is a reviewed dependency: when either side declares
|
|
72
|
+
// it, both must, and they must agree (a code-only alias would ride a
|
|
73
|
+
// connection reviewers never saw).
|
|
74
|
+
add(
|
|
75
|
+
'credential-alias',
|
|
76
|
+
'credential alias matches the manifest (when either declares it)',
|
|
77
|
+
(provider.credentialProvider ?? null) === (manifest.credential_provider ?? null),
|
|
78
|
+
`code '${provider.credentialProvider ?? '—'}' vs manifest '${manifest.credential_provider ?? '—'}'`,
|
|
79
|
+
);
|
|
71
80
|
|
|
72
81
|
// 6 — code may not claim MORE network than the reviewed manifest.
|
|
73
82
|
const undeclared = (provider.allowedDomains ?? []).filter(
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,23 @@ export type AuthKind = 'oauth2' | 'api_key' | 'app_password';
|
|
|
22
22
|
// to 'ready'), so legacy adapters stay connectable until told otherwise.
|
|
23
23
|
export type Maturity = 'ready' | 'coming_soon';
|
|
24
24
|
|
|
25
|
+
// Who a connector is FOR — the audience axis the end-user client uses to decide
|
|
26
|
+
// whether to surface it, and how. Classify EVERY connector (defineProvider will
|
|
27
|
+
// enforce this) so the client catalog never conflates the three kinds:
|
|
28
|
+
// 'user-account' → the tenant's OWN account; connect/disconnect is the point.
|
|
29
|
+
// Shown in the end-user connections hub (Gmail, Slack, Stripe,
|
|
30
|
+
// HubSpot, social channels).
|
|
31
|
+
// 'agent-tool' → infrastructure an AGENT uses to do a task, not a personal
|
|
32
|
+
// account. The platform may hold a shared key; a tenant may
|
|
33
|
+
// override with their own. NEVER a standalone hub card — it
|
|
34
|
+
// surfaces only via the install card of an agent that declares
|
|
35
|
+
// it (Apollo, Hunter, Clearbit — enrichment/analytics/scraping).
|
|
36
|
+
// 'internal' → platform plumbing, or a dormant connector with no operations
|
|
37
|
+
// and no consumer. Never shown to end users (e.g. agent-sessions;
|
|
38
|
+
// op-less stubs — also keep those at maturity:'coming_soon').
|
|
39
|
+
// See docs/architecture/05 + 15 and the AGENTS.md "Connectors" convention.
|
|
40
|
+
export type ConnectorSurface = 'user-account' | 'agent-tool' | 'internal';
|
|
41
|
+
|
|
25
42
|
export interface TokenSet {
|
|
26
43
|
accessToken: string;
|
|
27
44
|
refreshToken?: string | null;
|
|
@@ -95,6 +112,12 @@ export interface ConnectionProvider {
|
|
|
95
112
|
// Connect-path maturity. Omitted ⇒ 'ready'. See Maturity above.
|
|
96
113
|
maturity?: Maturity;
|
|
97
114
|
|
|
115
|
+
// Audience classification — see ConnectorSurface. Drives whether the end-user
|
|
116
|
+
// client surfaces this connector and how. Optional in the type for now so
|
|
117
|
+
// existing/external adapters compile through the SDK bump; defineProvider warns
|
|
118
|
+
// when omitted and this tightens to required once every built-in declares it.
|
|
119
|
+
surface?: ConnectorSurface;
|
|
120
|
+
|
|
98
121
|
// Egress allowlist: the ONLY hosts this connector's operations/test may reach
|
|
99
122
|
// through ctx.fetch. Bare hostname for an exact match ('api.acme.com') or a
|
|
100
123
|
// wildcard for subdomains ('*.acme.com' — does NOT match the apex; list it
|
|
@@ -107,6 +130,17 @@ export interface ConnectionProvider {
|
|
|
107
130
|
// omit it (first-party).
|
|
108
131
|
publisher?: string;
|
|
109
132
|
|
|
133
|
+
// Credential alias — the agent-tool chain's "one key opens many doors" seam.
|
|
134
|
+
// A connector that fronts one facet of a wider platform (an Apify actor, a
|
|
135
|
+
// Composio toolkit) declares the provider whose TENANT CONNECTION it rides:
|
|
136
|
+
// when a tenant has no row for THIS provider, getConnection falls back to the
|
|
137
|
+
// alias provider's row (tenant connects 'apify' once; every actor connector
|
|
138
|
+
// works). A tenant-pasted key for this provider still wins — the alias is the
|
|
139
|
+
// fallback, not the override. Refresh semantics follow the ALIAS provider's
|
|
140
|
+
// adapter. One hop only; the alias must name a concrete provider, never
|
|
141
|
+
// another aliased one.
|
|
142
|
+
credentialProvider?: string;
|
|
143
|
+
|
|
110
144
|
// Inline connect-form spec for api_key / app_password shapes. Omitted by pure
|
|
111
145
|
// oauth2 adapters (they render a Connect button instead). For a dual-mode
|
|
112
146
|
// provider (WordPress), this describes the NON-oauth fallback form; the UI
|
|
@@ -267,6 +301,16 @@ export function defineProvider(p: ConnectionProvider): ConnectionProvider {
|
|
|
267
301
|
if (typeof p.test !== 'function') {
|
|
268
302
|
throw new Error(`connections: provider '${p.provider}' must implement test()`);
|
|
269
303
|
}
|
|
304
|
+
if (!p.surface) {
|
|
305
|
+
// Nudge, not throw (yet): the built-ins all declare `surface`, but external/marketplace
|
|
306
|
+
// connectors (separate repos) haven't adopted it. Tightening to a hard throw + a required
|
|
307
|
+
// field is gated on adding `surface` to the connector manifest schema.
|
|
308
|
+
console.warn(
|
|
309
|
+
`connections: provider '${p.provider}' does not declare surface ` +
|
|
310
|
+
`('user-account' | 'agent-tool' | 'internal') — classify it so the client catalog knows ` +
|
|
311
|
+
`whether and how to show it. See the AGENTS.md "Connectors" convention.`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
270
314
|
if (p.operations && Object.keys(p.operations).length > 0 && !p.allowedDomains?.length) {
|
|
271
315
|
// Warn, don't throw: built-ins are grandfathered on ambient fetch. The
|
|
272
316
|
// marketplace publish gate REQUIRES allowed_domains in the manifest, so
|
package/src/manifest.ts
CHANGED
|
@@ -59,9 +59,24 @@ export const connectorManifestSchema = z
|
|
|
59
59
|
// tenant's credential — which is why remote connectors require explicit
|
|
60
60
|
// per-connection consent before any call.
|
|
61
61
|
runtime_url: z.string().url().max(500).optional(),
|
|
62
|
+
// Lane 'mcp': the author points at a remote Model Context Protocol server
|
|
63
|
+
// instead of writing operation bodies. Each declared operation forwards to
|
|
64
|
+
// that server's `tools/call` over Streamable HTTP, driven through the same
|
|
65
|
+
// governed fetch as Lane B. `url` is https-only and author-declared (never
|
|
66
|
+
// tenant-supplied); `auth` is 'injected' only in v1 — core vaults the
|
|
67
|
+
// tenant's credential and rides it on a transport header, never in the tool
|
|
68
|
+
// args. ('server_managed', where the MCP server holds its own upstream
|
|
69
|
+
// OAuth, is reserved for a later, higher-tier release.)
|
|
70
|
+
mcp: z
|
|
71
|
+
.object({
|
|
72
|
+
url: z.string().url().max(500),
|
|
73
|
+
auth: z.enum(['injected']),
|
|
74
|
+
})
|
|
75
|
+
.optional(),
|
|
62
76
|
// Remote connectors have no adapter code in core, so their connect form is
|
|
63
77
|
// declared HERE (vendored connectors keep connectFields in code — the
|
|
64
|
-
// adapter stays the single source of truth for Lane A).
|
|
78
|
+
// adapter stays the single source of truth for Lane A). MCP connectors are
|
|
79
|
+
// the same: no adapter code, so the form lives here too.
|
|
65
80
|
connect_fields: z
|
|
66
81
|
.array(
|
|
67
82
|
z.object({
|
|
@@ -77,6 +92,14 @@ export const connectorManifestSchema = z
|
|
|
77
92
|
allowed_domains: z
|
|
78
93
|
.array(z.string().regex(domainPattern, 'each entry is a bare host or *.host, optional :port'))
|
|
79
94
|
.min(1, 'declare at least one allowed domain'),
|
|
95
|
+
// Credential alias (see ConnectionProvider.credentialProvider): the provider
|
|
96
|
+
// whose tenant connection this connector rides when the tenant has not
|
|
97
|
+
// connected THIS provider directly. Declared here so reviewers see the
|
|
98
|
+
// dependency; the adapter's copy is authoritative at runtime.
|
|
99
|
+
credential_provider: z
|
|
100
|
+
.string()
|
|
101
|
+
.regex(kebab, 'credential_provider must be a lowercase-kebab provider key')
|
|
102
|
+
.optional(),
|
|
80
103
|
// Connector-level config keys the platform must supply (resolved from the
|
|
81
104
|
// encrypted config store — values are set by staff at publish, never bundled).
|
|
82
105
|
env: z
|
|
@@ -91,10 +114,19 @@ export const connectorManifestSchema = z
|
|
|
91
114
|
changelog: z.string().max(5000).optional(),
|
|
92
115
|
})
|
|
93
116
|
.superRefine((m, ctx) => {
|
|
94
|
-
|
|
117
|
+
const lanes = [m.entrypoint, m.runtime_url, m.mcp].filter((v) => v != null).length;
|
|
118
|
+
if (lanes === 0) {
|
|
95
119
|
ctx.addIssue({
|
|
96
120
|
code: 'custom',
|
|
97
|
-
message:
|
|
121
|
+
message:
|
|
122
|
+
'declare exactly one lane: an entrypoint (vendored, Lane A), a runtime_url (remote, Lane B), or an mcp endpoint (remote MCP)',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (lanes > 1) {
|
|
126
|
+
ctx.addIssue({
|
|
127
|
+
code: 'custom',
|
|
128
|
+
message:
|
|
129
|
+
'declare exactly one lane — entrypoint, runtime_url, and mcp are mutually exclusive',
|
|
98
130
|
});
|
|
99
131
|
}
|
|
100
132
|
if (m.runtime_url && !m.runtime_url.startsWith('https://')) {
|
|
@@ -116,6 +148,25 @@ export const connectorManifestSchema = z
|
|
|
116
148
|
'remote connectors must declare connect_fields (there is no adapter code to own the form)',
|
|
117
149
|
});
|
|
118
150
|
}
|
|
151
|
+
if (m.mcp && !m.mcp.url.startsWith('https://')) {
|
|
152
|
+
ctx.addIssue({ code: 'custom', path: ['mcp', 'url'], message: 'mcp.url must be https' });
|
|
153
|
+
}
|
|
154
|
+
if (m.mcp && m.auth_kind === 'oauth2') {
|
|
155
|
+
ctx.addIssue({
|
|
156
|
+
code: 'custom',
|
|
157
|
+
path: ['auth_kind'],
|
|
158
|
+
message:
|
|
159
|
+
'mcp connectors are api_key/app_password only — core injects the vaulted credential on a transport header, it does not broker an author-owned OAuth dance',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (m.mcp && (!m.connect_fields || m.connect_fields.length === 0)) {
|
|
163
|
+
ctx.addIssue({
|
|
164
|
+
code: 'custom',
|
|
165
|
+
path: ['connect_fields'],
|
|
166
|
+
message:
|
|
167
|
+
'mcp connectors must declare connect_fields (there is no adapter code to own the form)',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
119
170
|
});
|
|
120
171
|
|
|
121
172
|
export type ConnectorOperationDeclaration = z.infer<typeof connectorOperationDeclaration>;
|