@lanes-sh/link 0.1.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/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { defineProvider, type ProviderManifest } from '#connectivity';
|
|
5
|
+
import { ConfigError, layout } from '#profile';
|
|
6
|
+
import { findSecrets, formatSecretFindings } from '#profile';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provider manifests supplied by the operator.
|
|
10
|
+
*
|
|
11
|
+
* The built-in list is a convenience, never a boundary. Anything not shipped is
|
|
12
|
+
* a YAML file in `<workspace>/data/<profile>/providers.d/`, validated by the
|
|
13
|
+
* same schema the built-ins are — no code, no rebuild, no pull request.
|
|
14
|
+
*
|
|
15
|
+
* That is the whole scalability claim of this milestone: a service nobody has
|
|
16
|
+
* integrated costs a file.
|
|
17
|
+
*
|
|
18
|
+
* **Per profile**, which the workspace-wide directory this replaced was not. A
|
|
19
|
+
* manifest names a host, an OpenAPI document, and the credential refs that
|
|
20
|
+
* reach them, so it describes somebody's infrastructure — and work's is not
|
|
21
|
+
* personal's to read. The path comes from `layout` for the same reason every
|
|
22
|
+
* other profile-owned path does: one place that knows the on-disk shape.
|
|
23
|
+
* ADR-030.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface LoadedManifest {
|
|
27
|
+
readonly manifest: ProviderManifest;
|
|
28
|
+
readonly path: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadProfileProviders(
|
|
32
|
+
workspaceRoot: string,
|
|
33
|
+
profile: string,
|
|
34
|
+
): Promise<LoadedManifest[]> {
|
|
35
|
+
const directory = join(workspaceRoot, layout.providers(profile));
|
|
36
|
+
|
|
37
|
+
let entries: string[];
|
|
38
|
+
try {
|
|
39
|
+
entries = await readdir(directory);
|
|
40
|
+
} catch {
|
|
41
|
+
return []; // No custom providers is the normal case.
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const loaded: LoadedManifest[] = [];
|
|
45
|
+
|
|
46
|
+
for (const name of entries.sort()) {
|
|
47
|
+
if (!name.endsWith('.yaml') && !name.endsWith('.yml')) continue;
|
|
48
|
+
if (name.endsWith('.example.yaml')) continue;
|
|
49
|
+
|
|
50
|
+
const path = join(directory, name);
|
|
51
|
+
loaded.push({ manifest: await parseManifestFile(path), path });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return loaded;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function parseManifestFile(path: string): Promise<ProviderManifest> {
|
|
58
|
+
return resolveSpecPath(parseManifest(await readFile(path, 'utf8'), path), path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Make a relative `openapi` path mean what the operator meant.
|
|
63
|
+
*
|
|
64
|
+
* `docs/detailed/creating-a-provider.md` tells people to write `openapi: ./mything.json`,
|
|
65
|
+
* and until now that resolved against the *process* working directory — so it
|
|
66
|
+
* worked when you happened to run `lanes` from the right folder and failed
|
|
67
|
+
* with a confusing ENOENT everywhere else. The built-ins never noticed because
|
|
68
|
+
* they compute an absolute path from `import.meta.url`.
|
|
69
|
+
*
|
|
70
|
+
* Resolved against the manifest's own directory, the way one file referencing
|
|
71
|
+
* another normally works.
|
|
72
|
+
*/
|
|
73
|
+
function resolveSpecPath(manifest: ProviderManifest, source: string): ProviderManifest {
|
|
74
|
+
const connector = manifest.connector;
|
|
75
|
+
if (connector.kind !== 'http') return manifest;
|
|
76
|
+
if (/^https?:/i.test(connector.openapi) || isAbsolute(connector.openapi)) return manifest;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...manifest,
|
|
80
|
+
connector: { ...connector, openapi: resolve(dirname(source), connector.openapi) },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseManifest(text: string, source = '<manifest>'): ProviderManifest {
|
|
85
|
+
let raw: unknown;
|
|
86
|
+
try {
|
|
87
|
+
raw = parseYaml(text);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new ConfigError(`${source}: could not parse YAML — ${(error as Error).message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
93
|
+
throw new ConfigError(`${source}: expected a YAML mapping at the top level`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A manifest is config, so it is held to the same rule config is: references
|
|
97
|
+
// into the credential store, never values. Someone writing their first
|
|
98
|
+
// manifest will reach for pasting the API key directly, and this is what
|
|
99
|
+
// stops that becoming a credential in a file they might commit.
|
|
100
|
+
const secrets = findSecrets(raw);
|
|
101
|
+
if (secrets.length > 0) {
|
|
102
|
+
throw new ConfigError(
|
|
103
|
+
`${source}: ${formatSecretFindings(secrets)}`,
|
|
104
|
+
secrets.map((finding) => finding.path),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return defineProvider(raw);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new ConfigError(`${source}: ${(error as Error).message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A starting point, written out by `lanes link provider new`. */
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffold `lanes link` writes, one per connectivity type.
|
|
3
|
+
*
|
|
4
|
+
* A custom provider is the same declaration a built-in is — this is the *only*
|
|
5
|
+
* difference between `#providers/google/gmail/` and a file an operator drops in
|
|
6
|
+
* their profile's `providers.d/`, and the point of the whole manifest design.
|
|
7
|
+
* So the template offers one starting point per `connector.kind` rather than a
|
|
8
|
+
* single generic one that would have to be edited into shape.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function manifestTemplate(kind: 'mcp' | 'http' | 'imap' | 'dav' | 'fs'): string {
|
|
12
|
+
if (kind === 'mcp') {
|
|
13
|
+
return `# A custom MCP server — a vendor's, a colleague's, or your own.
|
|
14
|
+
#
|
|
15
|
+
# If the server supports Dynamic Client Registration there is nothing else to
|
|
16
|
+
# do: Lanes Link registers itself when you run \`lanes link connect\`.
|
|
17
|
+
id: acme
|
|
18
|
+
name: Acme
|
|
19
|
+
description: Whatever this server exposes.
|
|
20
|
+
|
|
21
|
+
connector:
|
|
22
|
+
kind: mcp
|
|
23
|
+
endpoint: https://mcp.acme.com/mcp
|
|
24
|
+
|
|
25
|
+
auth:
|
|
26
|
+
kind: oauth
|
|
27
|
+
registration: dynamic # or "manual" if the vendor makes you register an app
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (kind === 'imap') {
|
|
32
|
+
return `# Any mailbox: Fastmail, mailbox.org, a company Dovecot.
|
|
33
|
+
#
|
|
34
|
+
# There is no spec to read — IMAP describes its extensions but never its
|
|
35
|
+
# operations — so the capability set is fixed by the protocol. Reading never
|
|
36
|
+
# marks mail as read, and nothing here can delete any.
|
|
37
|
+
id: fastmail
|
|
38
|
+
name: Fastmail
|
|
39
|
+
|
|
40
|
+
connector:
|
|
41
|
+
kind: imap
|
|
42
|
+
host: imap.fastmail.com
|
|
43
|
+
port: 993
|
|
44
|
+
smtp:
|
|
45
|
+
host: smtp.fastmail.com
|
|
46
|
+
port: 465
|
|
47
|
+
starttls: false # 465 is implicit TLS; 587 upgrades in-band
|
|
48
|
+
|
|
49
|
+
auth:
|
|
50
|
+
kind: basic # a username and an app password, per account
|
|
51
|
+
|
|
52
|
+
identity:
|
|
53
|
+
kind: connector # the account is the name the server accepted
|
|
54
|
+
|
|
55
|
+
setup:
|
|
56
|
+
docs: "Create an app password at https://app.fastmail.com/settings/security/apppw"
|
|
57
|
+
prompts:
|
|
58
|
+
- key: username
|
|
59
|
+
label: Email address
|
|
60
|
+
scope: connection
|
|
61
|
+
field: username
|
|
62
|
+
- key: password
|
|
63
|
+
label: App password
|
|
64
|
+
secret: true
|
|
65
|
+
scope: connection
|
|
66
|
+
field: password
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (kind === 'fs') {
|
|
71
|
+
return `# A folder on this machine — iCloud Drive, Dropbox, a project directory.
|
|
72
|
+
#
|
|
73
|
+
# No credential: the permission is the operating system's, so this only works
|
|
74
|
+
# where the files actually are. Everything under the root is reachable and
|
|
75
|
+
# nothing above it is, symlinks included.
|
|
76
|
+
id: dropbox
|
|
77
|
+
name: Dropbox
|
|
78
|
+
|
|
79
|
+
connector:
|
|
80
|
+
kind: fs
|
|
81
|
+
root: ~/Dropbox
|
|
82
|
+
max_file_bytes: 262144 # one read; a large file is refused rather than truncated
|
|
83
|
+
exclude: [] # on top of .git, .ssh, node_modules, which are always refused
|
|
84
|
+
|
|
85
|
+
auth: { kind: none }
|
|
86
|
+
identity: { kind: connector }
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (kind === 'dav') {
|
|
91
|
+
return `# Any CalDAV or CardDAV server: Nextcloud, Radicale, Fastmail.
|
|
92
|
+
#
|
|
93
|
+
# Discovery walks .well-known, then the principal, then the collection home —
|
|
94
|
+
# and remembers the result per account, since many servers answer from a
|
|
95
|
+
# per-user host.
|
|
96
|
+
id: nextcloud_calendar
|
|
97
|
+
name: Nextcloud Calendar
|
|
98
|
+
|
|
99
|
+
connector:
|
|
100
|
+
kind: dav
|
|
101
|
+
base_url: https://cloud.example.com
|
|
102
|
+
service: caldav # or carddav, for contacts
|
|
103
|
+
|
|
104
|
+
auth:
|
|
105
|
+
kind: basic
|
|
106
|
+
|
|
107
|
+
identity:
|
|
108
|
+
kind: connector
|
|
109
|
+
|
|
110
|
+
setup:
|
|
111
|
+
docs: "Use an app password from Settings → Security."
|
|
112
|
+
prompts:
|
|
113
|
+
- key: username
|
|
114
|
+
label: Username
|
|
115
|
+
scope: connection
|
|
116
|
+
field: username
|
|
117
|
+
- key: password
|
|
118
|
+
label: App password
|
|
119
|
+
secret: true
|
|
120
|
+
scope: connection
|
|
121
|
+
field: password
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return `# A REST API described by OpenAPI.
|
|
126
|
+
#
|
|
127
|
+
# Operations become capabilities automatically — GET and HEAD land in the
|
|
128
|
+
# "read" bundle (granted by default), everything else in "write".
|
|
129
|
+
id: acme
|
|
130
|
+
name: Acme
|
|
131
|
+
description: Whatever this API exposes.
|
|
132
|
+
|
|
133
|
+
connector:
|
|
134
|
+
kind: http
|
|
135
|
+
base_url: https://api.acme.com/v1
|
|
136
|
+
openapi: https://api.acme.com/openapi.json # a URL, or a path in this workspace
|
|
137
|
+
operations:
|
|
138
|
+
# A large spec yields hundreds of tools, which no agent can reason over.
|
|
139
|
+
# Narrow it by operationId, path, or tag.
|
|
140
|
+
include: ["*Account*", "*Payment*"]
|
|
141
|
+
exclude: []
|
|
142
|
+
|
|
143
|
+
auth:
|
|
144
|
+
kind: header
|
|
145
|
+
header: X-API-Key
|
|
146
|
+
credential_ref: acme/api_key # a reference; never the key itself
|
|
147
|
+
|
|
148
|
+
setup:
|
|
149
|
+
docs: "Generate a key at https://acme.com/settings/api"
|
|
150
|
+
prompts:
|
|
151
|
+
- key: api_key
|
|
152
|
+
label: Acme API key
|
|
153
|
+
secret: true
|
|
154
|
+
credential_ref: acme/api_key
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineLocalProvider, keepKeys, type ProviderContext } from '#connectivity';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The example provider — the reference every other provider is written
|
|
6
|
+
* against, and small enough to reproduce verbatim in
|
|
7
|
+
* `docs/detailed/creating-a-provider.md`.
|
|
8
|
+
*
|
|
9
|
+
* It is also load-bearing beyond being a sample. It declares no auth
|
|
10
|
+
* requirement and touches no third-party service, which makes it an *owner
|
|
11
|
+
* provider* in miniature — the same shape memory, skills, and vault take in
|
|
12
|
+
* M3. If this works, that shape is proven before those exist, and `lanes link connect
|
|
13
|
+
* example` opening no browser proves the CLI does not assume OAuth-shaped
|
|
14
|
+
* setup.
|
|
15
|
+
*
|
|
16
|
+
* Note what is absent: no `connection` argument is declared anywhere. Core
|
|
17
|
+
* injects it, fills its enum per profile from resolved policy, and resolves it
|
|
18
|
+
* to `context.connection` before a handler runs. A provider never thinks about
|
|
19
|
+
* which account it is serving — that is ADR-001, and it is what lets one tool
|
|
20
|
+
* set scale to any number of accounts.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const NOTE_KEY_PREFIX = 'note:';
|
|
24
|
+
|
|
25
|
+
async function readNote(context: ProviderContext, key: string): Promise<string | null> {
|
|
26
|
+
return context.state.get(`${NOTE_KEY_PREFIX}${key}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const exampleProvider = defineLocalProvider({
|
|
30
|
+
id: 'example',
|
|
31
|
+
name: 'Example',
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
description:
|
|
34
|
+
'A trivial provider with no external service. Serves as the provider SDK reference and as a way to exercise connection isolation without any credentials.',
|
|
35
|
+
|
|
36
|
+
configSchema: z.object({}),
|
|
37
|
+
connectionSchema: z.object({}),
|
|
38
|
+
|
|
39
|
+
bundles: [
|
|
40
|
+
{
|
|
41
|
+
name: 'read',
|
|
42
|
+
description: 'Read notes and echo messages.',
|
|
43
|
+
oauth_scopes: [],
|
|
44
|
+
capabilities: ['echo', 'get_note', 'list_notes'],
|
|
45
|
+
default: true,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'write',
|
|
49
|
+
description: 'Create and modify notes.',
|
|
50
|
+
oauth_scopes: [],
|
|
51
|
+
capabilities: ['set_note', 'delete_note'],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
capabilities: [
|
|
56
|
+
{
|
|
57
|
+
kind: 'tool',
|
|
58
|
+
name: 'echo',
|
|
59
|
+
title: 'Echo a message',
|
|
60
|
+
description:
|
|
61
|
+
'Return the supplied message unchanged, prefixed with the connection it was routed to. Useful for confirming which account a call reached.',
|
|
62
|
+
inputSchema: z.object({
|
|
63
|
+
message: z.string().min(1).describe('Text to echo back'),
|
|
64
|
+
}),
|
|
65
|
+
// The message is the whole payload, so recording it verbatim is the
|
|
66
|
+
// useful choice here. A provider handling real content would not.
|
|
67
|
+
redact: keepKeys('message'),
|
|
68
|
+
async handler({ message }, context) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text', text: `[${context.connection.key}] ${message}` }],
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
kind: 'tool',
|
|
77
|
+
name: 'set_note',
|
|
78
|
+
title: 'Store a note',
|
|
79
|
+
description:
|
|
80
|
+
'Store a note against this connection. Notes are scoped to the connection, so the same key holds a different value on each one.',
|
|
81
|
+
inputSchema: z.object({
|
|
82
|
+
key: z.string().min(1).describe('Note identifier'),
|
|
83
|
+
value: z.string().describe('Note contents'),
|
|
84
|
+
}),
|
|
85
|
+
// The key is safe to record; the value is the content itself.
|
|
86
|
+
redact: keepKeys('key'),
|
|
87
|
+
async handler({ key, value }, context) {
|
|
88
|
+
await context.state.set(`${NOTE_KEY_PREFIX}${key}`, value);
|
|
89
|
+
context.audit.annotate({ bytes: value.length });
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: `Stored note "${key}" on ${context.connection.key}.` }],
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
kind: 'tool',
|
|
99
|
+
name: 'get_note',
|
|
100
|
+
title: 'Read a note',
|
|
101
|
+
description: 'Read a note stored against this connection.',
|
|
102
|
+
inputSchema: z.object({
|
|
103
|
+
key: z.string().min(1).describe('Note identifier'),
|
|
104
|
+
}),
|
|
105
|
+
redact: keepKeys('key'),
|
|
106
|
+
async handler({ key }, context) {
|
|
107
|
+
const value = await readNote(context, key);
|
|
108
|
+
|
|
109
|
+
if (value === null) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: `No note "${key}" on ${context.connection.key}.`,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { content: [{ type: 'text', text: value }] };
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
kind: 'tool',
|
|
127
|
+
name: 'delete_note',
|
|
128
|
+
title: 'Delete a note',
|
|
129
|
+
description: 'Remove a note from this connection.',
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
key: z.string().min(1).describe('Note identifier'),
|
|
132
|
+
}),
|
|
133
|
+
redact: keepKeys('key'),
|
|
134
|
+
async handler({ key }, context) {
|
|
135
|
+
await context.state.delete(`${NOTE_KEY_PREFIX}${key}`);
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: `Deleted note "${key}" from ${context.connection.key}.` }],
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
kind: 'tool',
|
|
144
|
+
name: 'list_notes',
|
|
145
|
+
title: 'List note keys',
|
|
146
|
+
description: 'List the keys of every note stored against this connection.',
|
|
147
|
+
inputSchema: z.object({}),
|
|
148
|
+
async handler(_input, context) {
|
|
149
|
+
const keys = (await context.state.keys())
|
|
150
|
+
.filter((key) => key.startsWith(NOTE_KEY_PREFIX))
|
|
151
|
+
.map((key) => key.slice(NOTE_KEY_PREFIX.length));
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text:
|
|
158
|
+
keys.length > 0
|
|
159
|
+
? keys.join('\n')
|
|
160
|
+
: `No notes on ${context.connection.key} yet.`,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A resource rather than a tool, deliberately — ADR-006.
|
|
169
|
+
*
|
|
170
|
+
* A note is read-oriented structured context addressed by a stable
|
|
171
|
+
* identifier, which is exactly what resources are for. Making everything a
|
|
172
|
+
* tool is the easy default and the wrong one; the distinction matters much
|
|
173
|
+
* more for M3's memory and skills than it does here, so it is worth
|
|
174
|
+
* exercising now while the cost of getting it wrong is nil.
|
|
175
|
+
*/
|
|
176
|
+
{
|
|
177
|
+
kind: 'resource',
|
|
178
|
+
name: 'note',
|
|
179
|
+
title: 'Note',
|
|
180
|
+
description: 'A stored note, addressed by key.',
|
|
181
|
+
uriTemplate: 'example://note/{key}',
|
|
182
|
+
mimeType: 'text/plain',
|
|
183
|
+
|
|
184
|
+
async list(context) {
|
|
185
|
+
const keys = await context.state.keys();
|
|
186
|
+
return keys
|
|
187
|
+
.filter((key) => key.startsWith(NOTE_KEY_PREFIX))
|
|
188
|
+
.map((key) => {
|
|
189
|
+
const name = key.slice(NOTE_KEY_PREFIX.length);
|
|
190
|
+
return { uri: `example://note/${encodeURIComponent(name)}`, name };
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async read(uri, params, context) {
|
|
195
|
+
const key = params['key'];
|
|
196
|
+
if (!key) throw new Error(`Malformed note URI: ${uri}`);
|
|
197
|
+
|
|
198
|
+
const value = await readNote(context, decodeURIComponent(key));
|
|
199
|
+
if (value === null) throw new Error(`No note "${key}" on ${context.connection.key}`);
|
|
200
|
+
|
|
201
|
+
return { uri, mimeType: 'text/plain', text: value };
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export default exampleProvider;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import { CALENDAR_IDENTITY, GOOGLE_APP, GOOGLE_OAUTH, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleSetup } from '../shared/setup.ts';
|
|
4
|
+
import { CALENDAR_REDACT } from './redact.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read and write events without holding the calendar.
|
|
8
|
+
*
|
|
9
|
+
* Google splits Calendar the way it splits Drive, and here the split is usable.
|
|
10
|
+
* `calendar.events` reaches every event on every calendar and reaches nothing
|
|
11
|
+
* else: it cannot create a calendar, delete one, or change who it is shared
|
|
12
|
+
* with. Those are `auth/calendar`, which is this provider's `mail.google.com`
|
|
13
|
+
* and is not requested.
|
|
14
|
+
*
|
|
15
|
+
* `calendar.readonly` sits beside it and is not redundant. Two of the reads
|
|
16
|
+
* accept nothing narrower — listing the calendars, and `freeBusy`, which is the
|
|
17
|
+
* "when am I free" primitive and answers across calendars whose contents the
|
|
18
|
+
* token may not read. Dropping it would cost the two operations an assistant
|
|
19
|
+
* needs most.
|
|
20
|
+
*
|
|
21
|
+
* `calendar.events` is marked broad in `../shared/scopes.ts` and should stay
|
|
22
|
+
* marked. Two narrower scopes do exist — `calendar.events.owned` and
|
|
23
|
+
* `calendar.app.created`, Calendar's answer to `drive.file` — and neither is
|
|
24
|
+
* usable: the vendored spec's per-operation `security` blocks name only the
|
|
25
|
+
* scopes Google published when it was generated, so requesting one would fail
|
|
26
|
+
* `specs.test.ts` and, more to the point, would fail at Google.
|
|
27
|
+
* `calendar.app.created` also has the `drive.file` disease in a worse form — it
|
|
28
|
+
* reaches only calendars this app made, and the calendar anyone means is the
|
|
29
|
+
* primary one they already had.
|
|
30
|
+
*
|
|
31
|
+
* Note the `base_url`: Calendar carries its version in the host, like Drive
|
|
32
|
+
* (`/calendar/v3` + `/calendars/{id}/events`). Copying the Sheets shape yields
|
|
33
|
+
* `/v3/v3/...` and a 404 on every call.
|
|
34
|
+
*/
|
|
35
|
+
const CALENDAR_SCOPES = [
|
|
36
|
+
'https://www.googleapis.com/auth/calendar.readonly',
|
|
37
|
+
'https://www.googleapis.com/auth/calendar.events',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const calendar = defineProvider({
|
|
41
|
+
id: 'calendar',
|
|
42
|
+
name: 'Google Calendar',
|
|
43
|
+
description:
|
|
44
|
+
'Read and write calendar events — list, search, create, reschedule, and cancel — and answer when you are free, via the Calendar REST API.',
|
|
45
|
+
connector: {
|
|
46
|
+
kind: 'http',
|
|
47
|
+
base_url: 'https://www.googleapis.com/calendar/v3',
|
|
48
|
+
openapi: specPath('calendar.v3.json'),
|
|
49
|
+
},
|
|
50
|
+
auth: {
|
|
51
|
+
kind: 'oauth',
|
|
52
|
+
registration: 'manual',
|
|
53
|
+
app: GOOGLE_APP,
|
|
54
|
+
scopes: CALENDAR_SCOPES,
|
|
55
|
+
...GOOGLE_OAUTH,
|
|
56
|
+
},
|
|
57
|
+
identity: CALENDAR_IDENTITY,
|
|
58
|
+
setup: googleSetup('Calendar', CALENDAR_SCOPES, {
|
|
59
|
+
// `calendar-json.googleapis.com`, not `calendar.googleapis.com`. The
|
|
60
|
+
// service is registered under its discovery host, and enabling the
|
|
61
|
+
// plausible-looking name leaves consent succeeding and every call
|
|
62
|
+
// answering 403 — the failure this whole setup block exists to prevent.
|
|
63
|
+
apis: ['calendar-json.googleapis.com'],
|
|
64
|
+
}),
|
|
65
|
+
redact: CALENDAR_REDACT,
|
|
66
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What survives into the audit log when an event is read or changed.
|
|
3
|
+
*
|
|
4
|
+
* The generated tools flatten a request body into top-level arguments, so
|
|
5
|
+
* `summary`, `description`, `location`, and `attendees` really are argument
|
|
6
|
+
* names here — and they are the meeting. Keeping them would put "1:1 re
|
|
7
|
+
* redundancy consultation" and the list of who was invited in a log meant to
|
|
8
|
+
* record that a change happened, not to hold a second copy of the calendar.
|
|
9
|
+
*
|
|
10
|
+
* `sendUpdates` is kept on every write, and it is the one flag worth reading
|
|
11
|
+
* back: cancelling a meeting quietly and cancelling it with mail to everyone
|
|
12
|
+
* invited are different acts, and only this argument tells them apart.
|
|
13
|
+
*
|
|
14
|
+
* `q` is withheld on Gmail's reasoning — a search is a question, and "who am I
|
|
15
|
+
* meeting about the redundancy" is content whoever typed it did not expect to
|
|
16
|
+
* keep. `freebusy.query`'s `items` is withheld on the same ground, which is the
|
|
17
|
+
* one call worth explaining: `calendarId` is kept everywhere else because it
|
|
18
|
+
* names the resource being read or written, but a `freeBusy` `items` list is a
|
|
19
|
+
* set of *people you asked about*. That is the query, not the subject.
|
|
20
|
+
*/
|
|
21
|
+
export const CALENDAR_REDACT: Record<string, string[]> = {
|
|
22
|
+
'calendarList.list': ['minAccessRole', 'showHidden', 'maxResults'],
|
|
23
|
+
'events.list': [
|
|
24
|
+
'calendarId',
|
|
25
|
+
'timeMin',
|
|
26
|
+
'timeMax',
|
|
27
|
+
'singleEvents',
|
|
28
|
+
'orderBy',
|
|
29
|
+
'maxResults',
|
|
30
|
+
'showDeleted',
|
|
31
|
+
'eventTypes',
|
|
32
|
+
],
|
|
33
|
+
'events.get': ['calendarId', 'eventId'],
|
|
34
|
+
'events.instances': ['calendarId', 'eventId', 'timeMin', 'timeMax', 'maxResults'],
|
|
35
|
+
'freebusy.query': ['timeMin', 'timeMax', 'timeZone'],
|
|
36
|
+
'events.insert': ['calendarId', 'sendUpdates', 'conferenceDataVersion'],
|
|
37
|
+
'events.patch': ['calendarId', 'eventId', 'sendUpdates'],
|
|
38
|
+
'events.delete': ['calendarId', 'eventId', 'sendUpdates'],
|
|
39
|
+
'events.move': ['calendarId', 'eventId', 'destination', 'sendUpdates'],
|
|
40
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import { GOOGLE_APP, GOOGLE_OAUTH, PEOPLE_IDENTITY, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleSetup } from '../shared/setup.ts';
|
|
4
|
+
import { CONTACTS_REDACT } from './redact.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read-only, so that "email Bob" resolves to an address.
|
|
8
|
+
*
|
|
9
|
+
* Two scopes, because Google keeps contacts in two places.
|
|
10
|
+
* `contacts.readonly` is the address book someone curated;
|
|
11
|
+
* `contacts.other.readonly` is where Gmail files an address written to but
|
|
12
|
+
* never saved, which is where most of the Bobs actually are. Asking for only
|
|
13
|
+
* the first would answer confidently and wrongly for the common case.
|
|
14
|
+
*
|
|
15
|
+
* Both are read-only, so neither is marked broad. The write scope is
|
|
16
|
+
* `contacts`, it permanently deletes, and it is not requested — nothing here
|
|
17
|
+
* writes. A contact card is the shape where a round trip silently destroys
|
|
18
|
+
* data, which is the same reason `icloud_contacts` declines to edit one.
|
|
19
|
+
*
|
|
20
|
+
* The provider is `contacts` while Google's API is People. That mismatch is
|
|
21
|
+
* load-bearing in `redact.ts` — see the note there — and it is deliberate: the
|
|
22
|
+
* thing an operator connects is their contacts, and `icloud_contacts` is
|
|
23
|
+
* already the neighbour in the list.
|
|
24
|
+
*/
|
|
25
|
+
const CONTACTS_SCOPES = [
|
|
26
|
+
'https://www.googleapis.com/auth/contacts.readonly',
|
|
27
|
+
'https://www.googleapis.com/auth/contacts.other.readonly',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const contacts = defineProvider({
|
|
31
|
+
id: 'contacts',
|
|
32
|
+
name: 'Google Contacts',
|
|
33
|
+
description:
|
|
34
|
+
'Look up a saved contact by name to find their address or phone number, including the addresses Gmail saved automatically. Read-only, via the People REST API.',
|
|
35
|
+
connector: {
|
|
36
|
+
kind: 'http',
|
|
37
|
+
base_url: 'https://people.googleapis.com',
|
|
38
|
+
openapi: specPath('people.v1.json'),
|
|
39
|
+
},
|
|
40
|
+
auth: {
|
|
41
|
+
kind: 'oauth',
|
|
42
|
+
registration: 'manual',
|
|
43
|
+
app: GOOGLE_APP,
|
|
44
|
+
scopes: CONTACTS_SCOPES,
|
|
45
|
+
...GOOGLE_OAUTH,
|
|
46
|
+
},
|
|
47
|
+
identity: PEOPLE_IDENTITY,
|
|
48
|
+
setup: googleSetup('Contacts', CONTACTS_SCOPES, { apis: ['people.googleapis.com'] }),
|
|
49
|
+
redact: CONTACTS_REDACT,
|
|
50
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What survives into the audit log when contacts are searched.
|
|
3
|
+
*
|
|
4
|
+
* The keys carry a `people.` prefix that no other Google provider here needs,
|
|
5
|
+
* and it is not a typo. `shortenName` strips the *provider id* from an
|
|
6
|
+
* operationId, and this provider is `contacts` while Google's operationIds all
|
|
7
|
+
* say `people` — so nothing is stripped, and the capability really is called
|
|
8
|
+
* `people.people.searchContacts`. A key written `people.searchContacts` would
|
|
9
|
+
* match nothing, withhold every value, and look exactly like redaction working.
|
|
10
|
+
*
|
|
11
|
+
* `query` is withheld throughout. A name someone is looking up is content, and
|
|
12
|
+
* it is the line `icloud_contacts.search_contacts` already draws. What is kept
|
|
13
|
+
* is the shape of the request: how many were asked for, which fields, and — on
|
|
14
|
+
* `getBatchGet` — the opaque resource names, which are identifiers rather than
|
|
15
|
+
* anything a person wrote.
|
|
16
|
+
*/
|
|
17
|
+
export const CONTACTS_REDACT: Record<string, string[]> = {
|
|
18
|
+
'people.people.searchContacts': ['pageSize', 'readMask'],
|
|
19
|
+
'people.otherContacts.search': ['pageSize', 'readMask'],
|
|
20
|
+
'people.people.getBatchGet': ['resourceNames', 'personFields'],
|
|
21
|
+
};
|