@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,362 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMcpHandler,
|
|
3
|
+
type McpHttpHandler,
|
|
4
|
+
type McpRequestContext,
|
|
5
|
+
} from '@modelcontextprotocol/server';
|
|
6
|
+
import { ownerPrincipal, type Principal } from '#auth';
|
|
7
|
+
import type { Logger } from '#connectivity';
|
|
8
|
+
import { clearUpstreamTokens } from '#connectivity/auth/index.ts';
|
|
9
|
+
import {
|
|
10
|
+
buildMcpServer,
|
|
11
|
+
toolNameFor,
|
|
12
|
+
visibleCapabilities,
|
|
13
|
+
type ProfileRuntime,
|
|
14
|
+
} from '#server/mcp';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Which runtimes are current, and when the ones they replaced are closed.
|
|
18
|
+
*
|
|
19
|
+
* This is a different subject from what an HTTP request does, which is why it
|
|
20
|
+
* is not in `index.ts`. The endpoint used to hold one map of profile runtimes
|
|
21
|
+
* for the life of the process, and every cache derived from it — the handler
|
|
22
|
+
* memo, the visible-tool set, the capability-id list, the skill poll clock —
|
|
23
|
+
* hung off the request handler beside it. That was correct while the map could
|
|
24
|
+
* not change.
|
|
25
|
+
*
|
|
26
|
+
* It can now: `connect` writes a connection and tells the endpoint, and the
|
|
27
|
+
* endpoint re-reads its whole config (ADR-029). Once the map can be replaced,
|
|
28
|
+
* every one of those caches becomes a separate thing that must be invalidated
|
|
29
|
+
* at exactly the same moment, and the handler memo is the one that bites — it
|
|
30
|
+
* closes over the map *inside* `buildMcpServer`, so a stale entry serves the
|
|
31
|
+
* old config from a handler that looks fresh.
|
|
32
|
+
*
|
|
33
|
+
* So the caches move onto the generation they were derived from. Replacing the
|
|
34
|
+
* generation replaces all of them together, and nothing stale can outlive its
|
|
35
|
+
* generation by construction rather than by remembering to clear it.
|
|
36
|
+
*
|
|
37
|
+
* ## Why a generation is not closed when it is replaced
|
|
38
|
+
*
|
|
39
|
+
* `Runtime.close()` ends connector sessions and writes the audit log's end
|
|
40
|
+
* marker. A request that started against a generation is still using both, and
|
|
41
|
+
* requests do not stop at a reload — so a generation is *retired* when it is
|
|
42
|
+
* replaced and closed when its last in-flight request has drained. `fetch`
|
|
43
|
+
* pins one for its whole lifetime; that pin is the reference count.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** A workspace opened and reconciled: what it serves, and how to let it go. */
|
|
47
|
+
export interface OpenedWorkspace {
|
|
48
|
+
readonly profiles: ReadonlyMap<string, ProfileRuntime>;
|
|
49
|
+
close(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Re-open and re-reconcile the whole workspace. Supplied by `endpoint.ts`. */
|
|
53
|
+
export type OpenWorkspace = () => Promise<OpenedWorkspace>;
|
|
54
|
+
|
|
55
|
+
export interface GenerationDeps {
|
|
56
|
+
/** Which profile's principal the visible-tool set is computed for. */
|
|
57
|
+
readonly primary: string;
|
|
58
|
+
readonly log: Logger;
|
|
59
|
+
readonly version?: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One boot's worth of runtimes, and everything derived from them.
|
|
64
|
+
*
|
|
65
|
+
* Immutable in what it serves. The memos inside still recompute on
|
|
66
|
+
* `registry.revision`, because skills can be replaced *within* a generation
|
|
67
|
+
* (ADR-014) — that is the one mutable surface the registry has, and it predates
|
|
68
|
+
* this.
|
|
69
|
+
*/
|
|
70
|
+
export class Generation {
|
|
71
|
+
readonly epoch: number;
|
|
72
|
+
readonly profiles: ReadonlyMap<string, ProfileRuntime>;
|
|
73
|
+
|
|
74
|
+
readonly #opened: OpenedWorkspace;
|
|
75
|
+
readonly #deps: GenerationDeps;
|
|
76
|
+
readonly #handlers = new Map<string, McpHttpHandler>();
|
|
77
|
+
|
|
78
|
+
/** In-flight requests pinned to this generation. */
|
|
79
|
+
#pins = 0;
|
|
80
|
+
/** Replaced by a newer generation, so it closes when the last pin drops. */
|
|
81
|
+
#retired = false;
|
|
82
|
+
#closed = false;
|
|
83
|
+
|
|
84
|
+
/** How stale a registry may be before the next request re-reads its skills. */
|
|
85
|
+
static readonly SKILL_POLL_MS = 2_000;
|
|
86
|
+
#polledAt = 0;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Every capability id across every profile, granted or not.
|
|
90
|
+
*
|
|
91
|
+
* Used only to spell a refusal correctly: a tool that exists but is not
|
|
92
|
+
* permitted should appear in the audit under its real id.
|
|
93
|
+
*/
|
|
94
|
+
readonly allCapabilityIds: () => readonly string[];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wire names the endpoint advertises.
|
|
98
|
+
*
|
|
99
|
+
* M1 has a single principal per profile, so this set does not vary by caller.
|
|
100
|
+
* When delegated principals arrive it becomes a per-principal lookup; the call
|
|
101
|
+
* site already reads as one.
|
|
102
|
+
*/
|
|
103
|
+
readonly visible: () => ReadonlySet<string>;
|
|
104
|
+
|
|
105
|
+
constructor(epoch: number, opened: OpenedWorkspace, deps: GenerationDeps) {
|
|
106
|
+
this.epoch = epoch;
|
|
107
|
+
this.profiles = opened.profiles;
|
|
108
|
+
this.#opened = opened;
|
|
109
|
+
this.#deps = deps;
|
|
110
|
+
|
|
111
|
+
this.allCapabilityIds = this.#memo(() => [
|
|
112
|
+
...new Set(
|
|
113
|
+
[...this.profiles.values()].flatMap((runtime) =>
|
|
114
|
+
runtime.registry.capabilities().map(({ id }) => id),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
this.visible = this.#memo(
|
|
120
|
+
() =>
|
|
121
|
+
new Set(
|
|
122
|
+
visibleCapabilities({
|
|
123
|
+
profiles: this.profiles,
|
|
124
|
+
principal: ownerPrincipal(deps.primary),
|
|
125
|
+
}).map(toolNameFor),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The profile names this generation serves, in declaration order. */
|
|
131
|
+
names(): string[] {
|
|
132
|
+
return [...this.profiles.keys()];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pin(): void {
|
|
136
|
+
this.#pins += 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Drop a pin, closing the generation if it was retired and this was the last. */
|
|
140
|
+
async unpin(): Promise<void> {
|
|
141
|
+
this.#pins -= 1;
|
|
142
|
+
if (this.#retired && this.#pins <= 0) await this.close();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Mark superseded. Closes immediately when nothing is using it. */
|
|
146
|
+
async retire(): Promise<void> {
|
|
147
|
+
this.#retired = true;
|
|
148
|
+
if (this.#pins <= 0) await this.close();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async close(): Promise<void> {
|
|
152
|
+
if (this.#closed) return;
|
|
153
|
+
this.#closed = true;
|
|
154
|
+
|
|
155
|
+
await Promise.all([...this.#handlers.values()].map((handler) => handler.close()));
|
|
156
|
+
this.#handlers.clear();
|
|
157
|
+
await this.#opened.close();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Work derived from the registries, recomputed when one of them changes.
|
|
162
|
+
*
|
|
163
|
+
* Skills can be replaced in a registry (ADR-014), and a stale `visible()` is
|
|
164
|
+
* not cosmetic: it gates the refusal-audit path, so a newly added skill would
|
|
165
|
+
* be recorded as a refusal on its first `prompts/get` even though the call
|
|
166
|
+
* succeeded.
|
|
167
|
+
*/
|
|
168
|
+
#generation(): number {
|
|
169
|
+
return [...this.profiles.values()].reduce(
|
|
170
|
+
(total, runtime) => total + runtime.registry.revision,
|
|
171
|
+
0,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#memo<T>(compute: () => T): () => T {
|
|
176
|
+
let at = -1;
|
|
177
|
+
let value: T;
|
|
178
|
+
return () => {
|
|
179
|
+
const now = this.#generation();
|
|
180
|
+
if (now !== at) {
|
|
181
|
+
value = compute();
|
|
182
|
+
at = now;
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Re-read the skills, at most once per poll interval.
|
|
190
|
+
*
|
|
191
|
+
* A skill written elsewhere — `lanes link skills add` in another terminal —
|
|
192
|
+
* cannot announce itself, so the endpoint has to look. Bounded rather than
|
|
193
|
+
* per-request because looking costs a `list()`, which on S3 is a network call.
|
|
194
|
+
* A write made *through* MCP does not wait for this; it refreshes directly.
|
|
195
|
+
*/
|
|
196
|
+
async refreshSkills(): Promise<void> {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
if (now - this.#polledAt < Generation.SKILL_POLL_MS) return;
|
|
199
|
+
this.#polledAt = now;
|
|
200
|
+
|
|
201
|
+
await Promise.all(
|
|
202
|
+
[...this.profiles.values()].map(async (runtime) => {
|
|
203
|
+
try {
|
|
204
|
+
await runtime.refreshSkills?.();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// A skills directory that has gone unreadable, or one skill file
|
|
207
|
+
// someone is mid-edit, must not take the endpoint down with it. The
|
|
208
|
+
// previously loaded skills stay registered.
|
|
209
|
+
this.#deps.log.warn('could not refresh skills', { message: (error as Error).message });
|
|
210
|
+
}
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* One handler per (principal, client label), memoised within this generation.
|
|
217
|
+
*
|
|
218
|
+
* The MCP surface depends only on resolved policy, so rebuilding the wiring
|
|
219
|
+
* per request would be pure waste. Reuse is safe because `createMcpHandler`
|
|
220
|
+
* still constructs a fresh server instance per request — what is memoised is
|
|
221
|
+
* the factory wiring, never session state. Memoised *here* rather than on the
|
|
222
|
+
* request handler because the factory closes over this generation's profiles:
|
|
223
|
+
* a handler outliving its generation is the stale-config bug.
|
|
224
|
+
*/
|
|
225
|
+
handlerFor(principal: Principal, clientLabel: string | undefined): McpHttpHandler {
|
|
226
|
+
const key = `${principal.id}\u0000${clientLabel ?? ''}`;
|
|
227
|
+
const existing = this.#handlers.get(key);
|
|
228
|
+
if (existing) return existing;
|
|
229
|
+
|
|
230
|
+
const handler = createMcpHandler(
|
|
231
|
+
// The principal is closed over rather than read back out of `authInfo`:
|
|
232
|
+
// this handler is already keyed on it, and re-deriving identity from a
|
|
233
|
+
// field the SDK treats as opaque pass-through would create a second
|
|
234
|
+
// source of truth for who is calling.
|
|
235
|
+
(_context: McpRequestContext) =>
|
|
236
|
+
buildMcpServer({
|
|
237
|
+
profiles: this.profiles,
|
|
238
|
+
principal,
|
|
239
|
+
clientLabel,
|
|
240
|
+
...(this.#deps.version ? { version: this.#deps.version } : {}),
|
|
241
|
+
}),
|
|
242
|
+
{
|
|
243
|
+
onerror: (error: Error) =>
|
|
244
|
+
this.#deps.log.error('mcp handler error', { message: error.message }),
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
this.#handlers.set(key, handler);
|
|
249
|
+
return handler;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** What a reload did, as the `/reload` route reports it. */
|
|
254
|
+
export interface ReloadResult {
|
|
255
|
+
readonly reloaded: boolean;
|
|
256
|
+
readonly epoch: number;
|
|
257
|
+
readonly profiles: readonly string[];
|
|
258
|
+
/** Why it did not reload. Absent on success. */
|
|
259
|
+
readonly reason?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The current generation, and the reload that replaces it.
|
|
264
|
+
*
|
|
265
|
+
* One instance per endpoint. `acquire`/`release` bracket a request; `reload`
|
|
266
|
+
* swaps in a freshly opened workspace and retires the one it replaced.
|
|
267
|
+
*/
|
|
268
|
+
export class Generations {
|
|
269
|
+
#current: Generation;
|
|
270
|
+
#epoch: number;
|
|
271
|
+
#inFlight: Promise<ReloadResult> | null = null;
|
|
272
|
+
|
|
273
|
+
readonly #open: OpenWorkspace;
|
|
274
|
+
readonly #deps: GenerationDeps;
|
|
275
|
+
|
|
276
|
+
constructor(initial: OpenedWorkspace, open: OpenWorkspace, deps: GenerationDeps) {
|
|
277
|
+
this.#epoch = 0;
|
|
278
|
+
this.#current = new Generation(0, initial, deps);
|
|
279
|
+
this.#open = open;
|
|
280
|
+
this.#deps = deps;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get current(): Generation {
|
|
284
|
+
return this.#current;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Pin the current generation for the life of one request. */
|
|
288
|
+
acquire(): Generation {
|
|
289
|
+
const generation = this.#current;
|
|
290
|
+
generation.pin();
|
|
291
|
+
return generation;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
release(generation: Generation): Promise<void> {
|
|
295
|
+
return generation.unpin();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Re-open the workspace and serve what it now says.
|
|
300
|
+
*
|
|
301
|
+
* Serialised behind one in-flight promise: two concurrent `/reload` calls
|
|
302
|
+
* must not each open a full set of runtimes, and the second caller wants the
|
|
303
|
+
* same answer as the first rather than a second reload of the same edit.
|
|
304
|
+
*
|
|
305
|
+
* A failure is reported, never fatal. A config file caught mid-write, a
|
|
306
|
+
* bucket that briefly refuses a read, a profile someone is editing by hand —
|
|
307
|
+
* none of those are a reason to take down an endpoint that is serving. The
|
|
308
|
+
* previous generation stays current and the caller is told why.
|
|
309
|
+
*/
|
|
310
|
+
reload(): Promise<ReloadResult> {
|
|
311
|
+
if (this.#inFlight) return this.#inFlight;
|
|
312
|
+
|
|
313
|
+
const attempt = this.#reload().finally(() => {
|
|
314
|
+
this.#inFlight = null;
|
|
315
|
+
});
|
|
316
|
+
this.#inFlight = attempt;
|
|
317
|
+
return attempt;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async #reload(): Promise<ReloadResult> {
|
|
321
|
+
const previous = this.#current;
|
|
322
|
+
|
|
323
|
+
let opened: OpenedWorkspace;
|
|
324
|
+
try {
|
|
325
|
+
opened = await this.#open();
|
|
326
|
+
} catch (error) {
|
|
327
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
328
|
+
this.#deps.log.warn('could not reload config', { message: reason });
|
|
329
|
+
return {
|
|
330
|
+
reloaded: false,
|
|
331
|
+
epoch: previous.epoch,
|
|
332
|
+
profiles: previous.names(),
|
|
333
|
+
reason,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.#epoch += 1;
|
|
338
|
+
this.#current = new Generation(this.#epoch, opened, this.#deps);
|
|
339
|
+
|
|
340
|
+
// Module-global and keyed per connection, so it survives a reload that
|
|
341
|
+
// replaced everything else. Re-connecting `<provider>.<id>` to a different
|
|
342
|
+
// account would otherwise serve the previous account's access token until
|
|
343
|
+
// it expired — up to an hour after the config said otherwise. Unchanged
|
|
344
|
+
// connections pay one refresh.
|
|
345
|
+
clearUpstreamTokens();
|
|
346
|
+
|
|
347
|
+
// After the swap: a request arriving during the retire already gets the new
|
|
348
|
+
// generation, and this only waits on requests that started before it.
|
|
349
|
+
await previous.retire();
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
reloaded: true,
|
|
353
|
+
epoch: this.#current.epoch,
|
|
354
|
+
profiles: this.#current.names(),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Close whatever is current. In-flight pins are not waited on. */
|
|
359
|
+
close(): Promise<void> {
|
|
360
|
+
return this.#current.close();
|
|
361
|
+
}
|
|
362
|
+
}
|