@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keemakr/operator-sdk",
3
- "version": "0.2.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",
@@ -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
- if (!m.entrypoint && !m.runtime_url) {
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: 'declare an entrypoint (vendored, Lane A) or a runtime_url (remote, Lane B)',
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>;