@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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy evaluation.
|
|
3
|
+
*
|
|
4
|
+
* This is the product. `gmail.search = allow` and `gmail.send = deny` are
|
|
5
|
+
* decisions the runtime enforces, not instructions a model is asked to respect.
|
|
6
|
+
* Everything else in this codebase exists to make this module's answer binding.
|
|
7
|
+
*
|
|
8
|
+
* Two invariants, both tested:
|
|
9
|
+
*
|
|
10
|
+
* 1. DEFAULT DENY. Nothing is reachable unless a rule grants it. An empty
|
|
11
|
+
* policy grants nothing at all.
|
|
12
|
+
* 2. TIGHTEN ONLY. Composition can narrow, never widen. A profile rule can
|
|
13
|
+
* deny what the instance floor allowed; it can never allow what the floor
|
|
14
|
+
* withheld. The floor is empty in M1 — the invariant is implemented
|
|
15
|
+
* anyway, because it is what makes delegated access safe to add later,
|
|
16
|
+
* and retrofitting it once rules exist in the wild is not possible.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type { AuthorizationResult } from '#audit';
|
|
20
|
+
import type { AuthorizationResult } from '#audit';
|
|
21
|
+
|
|
22
|
+
export type { LimitConfig } from './limits.ts';
|
|
23
|
+
export { RateLimiter, DEFAULT_LIMITS } from './limits.ts';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `approval_required` is RESERVED. The model carries the state so that
|
|
27
|
+
* configuration written today stays loadable, but there is no approval engine
|
|
28
|
+
* and a rule carrying it is treated as `deny` until one exists — failing
|
|
29
|
+
* closed rather than silently permitting.
|
|
30
|
+
*/
|
|
31
|
+
export type Effect = 'allow' | 'deny' | 'approval_required';
|
|
32
|
+
|
|
33
|
+
export interface PolicyRule {
|
|
34
|
+
/** `*`, `gmail.*`, or `gmail.search`. Those three forms, and no others. */
|
|
35
|
+
readonly capability: string;
|
|
36
|
+
readonly effect: Effect;
|
|
37
|
+
readonly expiresAt?: Date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PolicyDocument {
|
|
41
|
+
readonly rules: readonly PolicyRule[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const EMPTY_POLICY: PolicyDocument = { rules: [] };
|
|
45
|
+
|
|
46
|
+
export interface PolicyRequest {
|
|
47
|
+
/**
|
|
48
|
+
* The authenticated principal. One per profile in M1 (the owner). Carried
|
|
49
|
+
* explicitly so that adding delegated principals later is new rows rather
|
|
50
|
+
* than a new signature on the dispatch path.
|
|
51
|
+
*/
|
|
52
|
+
readonly principal: string;
|
|
53
|
+
/** Fully qualified, e.g. `gmail.search`. */
|
|
54
|
+
readonly capability: string;
|
|
55
|
+
/**
|
|
56
|
+
* Fully qualified, e.g. `gmail.main`.
|
|
57
|
+
*
|
|
58
|
+
* Carried for audit, not for the decision: rules name capabilities only, so
|
|
59
|
+
* every account of a provider within a profile is governed identically.
|
|
60
|
+
* Granularity between accounts comes from a second profile, which shares no
|
|
61
|
+
* database, credential store, or URL with the first.
|
|
62
|
+
*/
|
|
63
|
+
readonly connection: string;
|
|
64
|
+
/** Defaults to now; injectable so expiry is testable without clock games. */
|
|
65
|
+
readonly at?: Date;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PolicyDecision {
|
|
69
|
+
readonly allowed: boolean;
|
|
70
|
+
readonly reason: AuthorizationResult;
|
|
71
|
+
readonly matched?: PolicyRule;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `*` matches everything, `gmail.*` matches one provider, otherwise literal. */
|
|
75
|
+
export function capabilityMatches(pattern: string, capability: string): boolean {
|
|
76
|
+
if (pattern === '*') return true;
|
|
77
|
+
if (pattern === capability) return true;
|
|
78
|
+
if (!pattern.endsWith('.*')) return false;
|
|
79
|
+
const prefix = pattern.slice(0, -1); // keep the dot: `gmail.*` -> `gmail.`
|
|
80
|
+
return capability.startsWith(prefix);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isRuleActive(rule: PolicyRule, at: Date): boolean {
|
|
84
|
+
return rule.expiresAt === undefined || rule.expiresAt > at;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findMatch(
|
|
88
|
+
document: PolicyDocument,
|
|
89
|
+
request: PolicyRequest,
|
|
90
|
+
at: Date,
|
|
91
|
+
effects: readonly Effect[],
|
|
92
|
+
): PolicyRule | undefined {
|
|
93
|
+
return document.rules.find(
|
|
94
|
+
(rule) =>
|
|
95
|
+
effects.includes(rule.effect) &&
|
|
96
|
+
isRuleActive(rule, at) &&
|
|
97
|
+
capabilityMatches(rule.capability, request.capability),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Evaluate one profile document. Deny wins over allow regardless of order, so
|
|
103
|
+
* rule ordering in a config file cannot change the answer — a denial is never
|
|
104
|
+
* something you can accidentally out-rank by putting an allow above it.
|
|
105
|
+
*/
|
|
106
|
+
export function evaluateDocument(
|
|
107
|
+
document: PolicyDocument,
|
|
108
|
+
request: PolicyRequest,
|
|
109
|
+
): PolicyDecision {
|
|
110
|
+
const at = request.at ?? new Date();
|
|
111
|
+
|
|
112
|
+
const denial = findMatch(document, request, at, ['deny', 'approval_required']);
|
|
113
|
+
if (denial) {
|
|
114
|
+
return { allowed: false, reason: 'denied_by_policy', matched: denial };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const allowance = findMatch(document, request, at, ['allow']);
|
|
118
|
+
if (allowance) {
|
|
119
|
+
return { allowed: true, reason: 'allowed', matched: allowance };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { allowed: false, reason: 'denied_default' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compose an optional instance floor with the profile's policy.
|
|
127
|
+
*
|
|
128
|
+
* Monotonically tightening: the floor is evaluated first and its denial is
|
|
129
|
+
* final. The profile document is only ever consulted to narrow further, so
|
|
130
|
+
* there is no arrangement of profile rules that can widen past the floor.
|
|
131
|
+
*/
|
|
132
|
+
export function evaluate(
|
|
133
|
+
request: PolicyRequest,
|
|
134
|
+
profile: PolicyDocument,
|
|
135
|
+
floor?: PolicyDocument,
|
|
136
|
+
): PolicyDecision {
|
|
137
|
+
if (floor) {
|
|
138
|
+
const floorDecision = evaluateDocument(floor, request);
|
|
139
|
+
if (!floorDecision.allowed) return floorDecision;
|
|
140
|
+
}
|
|
141
|
+
return evaluateDocument(profile, request);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The connections a principal may use for a given capability.
|
|
146
|
+
*
|
|
147
|
+
* This is what populates the `connection` argument enum, so a client cannot
|
|
148
|
+
* even discover a connection it has no grant for. Discovery filtering and
|
|
149
|
+
* invocation enforcement therefore share one implementation — if they were
|
|
150
|
+
* separate, they could disagree, and a leak in discovery is still a leak.
|
|
151
|
+
*/
|
|
152
|
+
export function allowedConnections(
|
|
153
|
+
capability: string,
|
|
154
|
+
connections: readonly string[],
|
|
155
|
+
principal: string,
|
|
156
|
+
profile: PolicyDocument,
|
|
157
|
+
floor?: PolicyDocument,
|
|
158
|
+
at?: Date,
|
|
159
|
+
): string[] {
|
|
160
|
+
// Only this capability's own provider. `gmail.search` must never offer a
|
|
161
|
+
// Notion account, and that constraint is structural rather than a policy
|
|
162
|
+
// decision — it used to fall out of rules naming a connection, so dropping
|
|
163
|
+
// that had to put it back explicitly.
|
|
164
|
+
const provider = capability.slice(0, capability.indexOf('.'));
|
|
165
|
+
const ofProvider = connections.filter((connection) => connection.startsWith(`${provider}.`));
|
|
166
|
+
|
|
167
|
+
// All or nothing beyond that, since rules do not discriminate between
|
|
168
|
+
// accounts. The list shape is kept because it is what the `connection` enum
|
|
169
|
+
// wants, and because a future principal-scoped rule would restore the
|
|
170
|
+
// filtering without touching any caller.
|
|
171
|
+
const first = ofProvider[0];
|
|
172
|
+
if (first === undefined) return [];
|
|
173
|
+
|
|
174
|
+
const request: PolicyRequest = at
|
|
175
|
+
? { principal, capability, connection: first, at }
|
|
176
|
+
: { principal, capability, connection: first };
|
|
177
|
+
|
|
178
|
+
return evaluate(request, profile, floor).allowed ? ofProvider : [];
|
|
179
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limits.
|
|
3
|
+
*
|
|
4
|
+
* These blunt a runaway agent retry loop and protect a vendor's quota. They are
|
|
5
|
+
* NOT a security boundary, and the guarantee table says so: an authorised caller
|
|
6
|
+
* that stays under the limit is not constrained by this module in any way.
|
|
7
|
+
*
|
|
8
|
+
* Limits are per instance. On Cloud Run, which scales horizontally, they are
|
|
9
|
+
* therefore not global — a shared counter store would be needed for that and is
|
|
10
|
+
* deliberately out of scope. Documented rather than silently misleading.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface LimitConfig {
|
|
14
|
+
/** Per profile — the endpoint as a whole. */
|
|
15
|
+
readonly requestsPerMinute: number;
|
|
16
|
+
/** Per connection — protects the upstream vendor's quota. */
|
|
17
|
+
readonly upstreamCallsPerMinute: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_LIMITS: LimitConfig = {
|
|
21
|
+
requestsPerMinute: 120,
|
|
22
|
+
upstreamCallsPerMinute: 60,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface Bucket {
|
|
26
|
+
tokens: number;
|
|
27
|
+
lastRefill: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Token bucket, refilled continuously rather than on a fixed window boundary.
|
|
32
|
+
*
|
|
33
|
+
* A fixed window lets a caller spend the whole budget in the last instant of
|
|
34
|
+
* one window and again in the first instant of the next, delivering double the
|
|
35
|
+
* intended rate exactly when a retry storm is worst.
|
|
36
|
+
*/
|
|
37
|
+
export class RateLimiter {
|
|
38
|
+
readonly #buckets = new Map<string, Bucket>();
|
|
39
|
+
readonly #now: () => number;
|
|
40
|
+
|
|
41
|
+
constructor(now: () => number = Date.now) {
|
|
42
|
+
this.#now = now;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Consume one token. Returns whether the call may proceed, and when the
|
|
47
|
+
* bucket will next have capacity — callers surface that as a retry hint
|
|
48
|
+
* rather than leaving an agent to guess.
|
|
49
|
+
*/
|
|
50
|
+
take(key: string, perMinute: number): { allowed: boolean; retryAfterMs: number } {
|
|
51
|
+
if (perMinute <= 0) return { allowed: false, retryAfterMs: 60_000 };
|
|
52
|
+
|
|
53
|
+
const now = this.#now();
|
|
54
|
+
const refillPerMs = perMinute / 60_000;
|
|
55
|
+
const bucket = this.#buckets.get(key) ?? { tokens: perMinute, lastRefill: now };
|
|
56
|
+
|
|
57
|
+
bucket.tokens = Math.min(perMinute, bucket.tokens + (now - bucket.lastRefill) * refillPerMs);
|
|
58
|
+
bucket.lastRefill = now;
|
|
59
|
+
|
|
60
|
+
if (bucket.tokens < 1) {
|
|
61
|
+
this.#buckets.set(key, bucket);
|
|
62
|
+
return { allowed: false, retryAfterMs: Math.ceil((1 - bucket.tokens) / refillPerMs) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
bucket.tokens -= 1;
|
|
66
|
+
this.#buckets.set(key, bucket);
|
|
67
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Drop idle buckets so a long-lived process does not accumulate keys forever. */
|
|
71
|
+
prune(idleMs = 300_000): void {
|
|
72
|
+
const cutoff = this.#now() - idleMs;
|
|
73
|
+
for (const [key, bucket] of this.#buckets) {
|
|
74
|
+
if (bucket.lastRefill < cutoff) this.#buckets.delete(key);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { credentialRef } from './primitives.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How a remote MCP client gets a token, when a bearer string is not enough.
|
|
6
|
+
*
|
|
7
|
+
* A bearer token is what `claude mcp add --header` and every local registration
|
|
8
|
+
* carry, and it stays the mechanism for those. It is not a mechanism a *remote*
|
|
9
|
+
* client can use: Claude's and ChatGPT's connector flows expect the OAuth
|
|
10
|
+
* handshake the MCP specification describes — a `401` naming a protected
|
|
11
|
+
* resource, discovery, an authorization code with PKCE — and offer nowhere to
|
|
12
|
+
* paste a fixed string.
|
|
13
|
+
*
|
|
14
|
+
* Two modes, because the choice is a real one and neither answer suits
|
|
15
|
+
* everybody:
|
|
16
|
+
*
|
|
17
|
+
* - `self` — this endpoint issues the tokens. It registers clients dynamically,
|
|
18
|
+
* so adding a connector is pasting the URL and approving once. Nothing to set
|
|
19
|
+
* up, which is why it is the documented default: an identity provider you must
|
|
20
|
+
* first go and configure in a console is the most expensive step in the whole
|
|
21
|
+
* product, and it is paid by every user.
|
|
22
|
+
* - `oidc` — an issuer you already run does it, and this endpoint only verifies.
|
|
23
|
+
* The cost is that the issuer must be reachable, must be registerable by the
|
|
24
|
+
* client (Google, for instance, supports neither dynamic registration nor
|
|
25
|
+
* client-ID metadata documents, so its client id has to be pasted in by hand),
|
|
26
|
+
* and must be told this endpoint's callback.
|
|
27
|
+
*
|
|
28
|
+
* Named for the protocol, not for a vendor — `issuer` is a URL, so pointing this
|
|
29
|
+
* at Entra, Auth0, or an internal provider is an edit rather than a code path.
|
|
30
|
+
* Absent means neither: bearer token only, exactly as before.
|
|
31
|
+
*/
|
|
32
|
+
const selfAuthorizationSchema = z.object({
|
|
33
|
+
mode: z.literal('self'),
|
|
34
|
+
/**
|
|
35
|
+
* How long an issued access token lives.
|
|
36
|
+
*
|
|
37
|
+
* Short by design and refreshed rather than lengthened: a client that holds a
|
|
38
|
+
* long-lived token has something worth stealing, and the refresh path is the
|
|
39
|
+
* one that can be revoked by dropping a row.
|
|
40
|
+
*/
|
|
41
|
+
access_token_ttl_minutes: z.number().int().positive().max(1440).default(60),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const oidcAuthorizationSchema = z.object({
|
|
45
|
+
mode: z.literal('oidc'),
|
|
46
|
+
/** Discovery root — `<issuer>/.well-known/openid-configuration` must resolve. */
|
|
47
|
+
issuer: z.url(),
|
|
48
|
+
/**
|
|
49
|
+
* The audience a presented token must name.
|
|
50
|
+
*
|
|
51
|
+
* Not decoration. Without it, any token the same issuer minted for any other
|
|
52
|
+
* application would open this endpoint — the confused-deputy case the MCP
|
|
53
|
+
* authorization spec calls out by name.
|
|
54
|
+
*/
|
|
55
|
+
client_id_ref: credentialRef,
|
|
56
|
+
/**
|
|
57
|
+
* Where to ask about a token, when the issuer's discovery document does not
|
|
58
|
+
* say.
|
|
59
|
+
*
|
|
60
|
+
* Access tokens are often opaque, so checking one means asking the issuer.
|
|
61
|
+
* RFC 7662 standardised that as `introspection_endpoint` in the metadata, and
|
|
62
|
+
* several large issuers ship an equivalent without advertising one. A URL
|
|
63
|
+
* here rather than a case in a switch — the same reasoning that puts the
|
|
64
|
+
* S3-compatible service endpoint in config rather than naming a vendor.
|
|
65
|
+
*/
|
|
66
|
+
introspection_endpoint: z.url().optional(),
|
|
67
|
+
/**
|
|
68
|
+
* Which subjects the issuer may vouch for. Matched against `sub` and against
|
|
69
|
+
* a verified `email`.
|
|
70
|
+
*
|
|
71
|
+
* At least one, always. An empty list is the kind of default that reads as
|
|
72
|
+
* "everyone" to whoever writes it and "no one" to whoever implemented it, and
|
|
73
|
+
* default deny is not a thing to leave ambiguous.
|
|
74
|
+
*/
|
|
75
|
+
allowed_subjects: z.array(z.string().min(1)).min(1),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const authorizationSchema = z.discriminatedUnion('mode', [
|
|
79
|
+
selfAuthorizationSchema,
|
|
80
|
+
oidcAuthorizationSchema,
|
|
81
|
+
]);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createFilesystemBlobStore } from '#deployments/adapters/filesystem.ts';
|
|
2
|
+
import { createGcsBlobStore } from '#deployments/adapters/gcs.ts';
|
|
3
|
+
import type { BlobStore } from '#stores/blobs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The workspace as a store, so a deployment's config does not have to be baked
|
|
7
|
+
* into its image.
|
|
8
|
+
*
|
|
9
|
+
* `LANES_LINK_HOME` used to be a directory and nothing else, which is why the
|
|
10
|
+
* Dockerfile copies `lanes-link.yaml` and `profiles/` in at build time. Its own
|
|
11
|
+
* comment records the trade and its expiry: *"the image **is** the config, so a
|
|
12
|
+
* revision fully describes what it serves and rollback is a revision switch.
|
|
13
|
+
* The cost is a rebuild per config change, which is the tradeoff docs/detailed/init.md
|
|
14
|
+
* accepts until it becomes annoying."*
|
|
15
|
+
*
|
|
16
|
+
* It became annoying. A policy change should not be a Docker build, and because
|
|
17
|
+
* those paths are gitignored the image cannot be built from a clean checkout at
|
|
18
|
+
* all — which is what stops there ever being one image that serves any
|
|
19
|
+
* workspace.
|
|
20
|
+
*
|
|
21
|
+
* So the root may be a `gs://` URL, and everything that reads config goes
|
|
22
|
+
* through a `BlobStore` rather than through `Bun.file`. There is no bootstrap
|
|
23
|
+
* problem: opening a GCS store needs only the URL and whatever identity is
|
|
24
|
+
* already present, and config carries secret *references* rather than values
|
|
25
|
+
* (`./secret-detection.ts` enforces that), so nothing has to be decrypted
|
|
26
|
+
* before the config that says how to decrypt things can be read.
|
|
27
|
+
*
|
|
28
|
+
* See ADR-023 for what this gives up — a Cloud Run revision no longer fully
|
|
29
|
+
* describes what it serves — and how the guarantee it kept is kept.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const GCS_SCHEME = 'gs://';
|
|
33
|
+
|
|
34
|
+
/** Whether a workspace root names a bucket rather than a directory. */
|
|
35
|
+
export function isRemoteWorkspace(root: string): boolean {
|
|
36
|
+
return root.startsWith(GCS_SCHEME);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The workspace's files.
|
|
41
|
+
*
|
|
42
|
+
* Rooted at the workspace, so every caller addresses `lanes-link.yaml` and
|
|
43
|
+
* `profiles/<name>.yaml` by the same relative key whichever backing it has.
|
|
44
|
+
*/
|
|
45
|
+
export function workspaceFiles(root: string): BlobStore {
|
|
46
|
+
if (!isRemoteWorkspace(root)) return createFilesystemBlobStore({ root });
|
|
47
|
+
|
|
48
|
+
const withoutScheme = root.slice(GCS_SCHEME.length);
|
|
49
|
+
const slash = withoutScheme.indexOf('/');
|
|
50
|
+
const bucket = slash === -1 ? withoutScheme : withoutScheme.slice(0, slash);
|
|
51
|
+
const prefix = slash === -1 ? '' : withoutScheme.slice(slash + 1);
|
|
52
|
+
|
|
53
|
+
if (bucket.length === 0) {
|
|
54
|
+
throw new Error(`LANES_LINK_HOME is "${root}", which names no bucket. Expected gs://<bucket>[/prefix].`);
|
|
55
|
+
}
|
|
56
|
+
return createGcsBlobStore({ bucket, ...(prefix ? { prefix } : {}) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read a UTF-8 document from the workspace, or null when it is not there. */
|
|
60
|
+
export async function readWorkspaceFile(files: BlobStore, key: string): Promise<string | null> {
|
|
61
|
+
const bytes = await files.get(key);
|
|
62
|
+
return bytes === null ? null : new TextDecoder().decode(bytes);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function writeWorkspaceFile(
|
|
66
|
+
files: BlobStore,
|
|
67
|
+
key: string,
|
|
68
|
+
text: string,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
await files.put(key, new TextEncoder().encode(text), { contentType: 'application/yaml' });
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A profile: its file, its schema, and where it lives on disk.
|
|
3
|
+
*
|
|
4
|
+
* **One profile = one config = one database = one credential store.** Profiles
|
|
5
|
+
* share an endpoint and its token (ADR-009); they never share state. This
|
|
6
|
+
* component owns everything about what a profile *is* — the YAML contract, the
|
|
7
|
+
* loader that validates it, and the workspace resolution that finds it.
|
|
8
|
+
*
|
|
9
|
+
* What it deliberately does not own: which providers exist (`#registry`) and
|
|
10
|
+
* how a call runs (`#dispatch`). Those were all one package called `core`,
|
|
11
|
+
* which was a name for "the rest of it" rather than for anything.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
SUPPORTED_CONTRACT,
|
|
16
|
+
configSchema,
|
|
17
|
+
workspaceSchema,
|
|
18
|
+
type AuthorizationConfig,
|
|
19
|
+
type Config,
|
|
20
|
+
type ConnectionConfig,
|
|
21
|
+
type DeployConfig,
|
|
22
|
+
type PolicyRuleConfig,
|
|
23
|
+
type TargetConfig,
|
|
24
|
+
type WorkspaceConfig,
|
|
25
|
+
} from './schema.ts';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
ConfigError,
|
|
29
|
+
loadConfigFile,
|
|
30
|
+
parseConfig,
|
|
31
|
+
validateConfig,
|
|
32
|
+
type LoadedConfig,
|
|
33
|
+
} from './load.ts';
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
findSecrets,
|
|
37
|
+
formatSecretFindings,
|
|
38
|
+
shannonEntropy,
|
|
39
|
+
type SecretFinding,
|
|
40
|
+
} from './secret-detection.ts';
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
WORKSPACE_FILE,
|
|
44
|
+
describeSelection,
|
|
45
|
+
installRoot,
|
|
46
|
+
listProfiles,
|
|
47
|
+
loadProfileConfig,
|
|
48
|
+
profilePath,
|
|
49
|
+
readWorkspace,
|
|
50
|
+
resolveSelection,
|
|
51
|
+
resolveWorkspaceRoot,
|
|
52
|
+
workspacePath,
|
|
53
|
+
} from './workspace.ts';
|
|
54
|
+
export {
|
|
55
|
+
TARGET_ENV,
|
|
56
|
+
askedTarget,
|
|
57
|
+
resolveDeployTarget,
|
|
58
|
+
resolveTarget,
|
|
59
|
+
undeclaredTarget,
|
|
60
|
+
} from './targets.ts';
|
|
61
|
+
export {
|
|
62
|
+
isRemoteWorkspace,
|
|
63
|
+
readWorkspaceFile,
|
|
64
|
+
workspaceFiles,
|
|
65
|
+
writeWorkspaceFile,
|
|
66
|
+
} from './files.ts';
|
|
67
|
+
export {
|
|
68
|
+
type Resolution,
|
|
69
|
+
type ResolveOptions,
|
|
70
|
+
} from './workspace.ts';
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
DATA_DIR,
|
|
74
|
+
layout,
|
|
75
|
+
profileDir,
|
|
76
|
+
} from './layout.ts';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a profile's data lives, in one place.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* ~/.lanes-link/
|
|
6
|
+
* ├── lanes-link.yaml which profiles exist, which is default
|
|
7
|
+
* ├── profiles/<name>.yaml each profile's declared config
|
|
8
|
+
* └── data/
|
|
9
|
+
* └── <profile>/ everything one profile owns
|
|
10
|
+
* ├── state.kv/ state, connections, cursors
|
|
11
|
+
* ├── audit.log/ one object per event
|
|
12
|
+
* ├── credentials.enc system credentials, and its .key
|
|
13
|
+
* ├── vault.enc the owner's items, its own key
|
|
14
|
+
* ├── skills.d/ procedures, one <name>/SKILL.md each
|
|
15
|
+
* ├── providers.d/ the operator's own provider manifests
|
|
16
|
+
* └── <provider>/<connection>/… whatever that provider stores
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* **One profile, one directory.** Before this, `data/` held
|
|
20
|
+
* `personal.db`, `personal.credentials.enc`, `personal.credentials.enc.key`,
|
|
21
|
+
* `work.db`, and a `personal/` directory, all in one flat listing — three
|
|
22
|
+
* profiles' worth of state interleaved, and no single thing to copy, back up, or
|
|
23
|
+
* delete. Now `rm -r data/work` is exactly "remove the work profile's data" and
|
|
24
|
+
* nothing else.
|
|
25
|
+
*
|
|
26
|
+
* **The blob root is the profile directory itself.** It used to be a `files/`
|
|
27
|
+
* subdirectory, which bought nothing and cost a level: a memory entry landed at
|
|
28
|
+
* `data/personal/files/memory/memory/entry/<id>.md`. The provider name and the
|
|
29
|
+
* connection name are the isolation boundary (`scopeBlobStore` namespaces
|
|
30
|
+
* `<provider>/<connection>`) and they are enough on their own, so an entry is
|
|
31
|
+
* now `data/personal/memory/main/<id>.md`. There is no collision risk with the
|
|
32
|
+
* files beside it: a provider id is `[a-z][a-z0-9_]*` and every reserved name
|
|
33
|
+
* here contains a dot.
|
|
34
|
+
*
|
|
35
|
+
* **Skills and provider manifests are the profile's too**, which reverses where
|
|
36
|
+
* both used to sit. They were at the workspace root, shared by every profile, on
|
|
37
|
+
* the reasoning that a procedure is not private to a profile the way its
|
|
38
|
+
* knowledge is. That was wrong twice over. A skill is instructions an agent will
|
|
39
|
+
* be handed, and ADR-014 §1 already treats authoring one as a grant worth
|
|
40
|
+
* governing — an odd thing to say about a document every profile reads anyway.
|
|
41
|
+
* And a procedure written for work names work's accounts, its people, and its
|
|
42
|
+
* conventions, so it is exactly as private as the knowledge it operates on.
|
|
43
|
+
* ADR-030 has the argument; ADR-009's "profiles share nothing" is what it
|
|
44
|
+
* restores.
|
|
45
|
+
*
|
|
46
|
+
* The `.d` suffix is the dot rule above rather than decoration. A plain
|
|
47
|
+
* `data/<profile>/skills/` is precisely the namespace the skills provider's own
|
|
48
|
+
* blobs scope into, so the one name that could not be used is the obvious one.
|
|
49
|
+
*
|
|
50
|
+
* These are **defaults**. A profile that declares its own paths keeps them.
|
|
51
|
+
*
|
|
52
|
+
* There is no migration from the layout this replaced, deliberately: a
|
|
53
|
+
* workspace is profiles, credentials, and whatever the owner has stored, and
|
|
54
|
+
* re-creating one is `lanes link profile add` plus `lanes link connect` per account. Machinery
|
|
55
|
+
* to move an old one would be more code than the thing it moves, and it would
|
|
56
|
+
* have to keep working forever to be worth having. Skills and manifests left at
|
|
57
|
+
* the old workspace-root paths are the same case: they stop loading, and moving
|
|
58
|
+
* them is one `mv` per profile that should see them.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/** The directory under the workspace root that holds every profile's data. */
|
|
62
|
+
export const DATA_DIR = 'data';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Everything one profile owns, relative to the workspace root.
|
|
66
|
+
*
|
|
67
|
+
* No leading `./`. It used to carry one, which `path.resolve` discards and an
|
|
68
|
+
* object key does not: a bucket read `./data/personal/state.kv/x` as a
|
|
69
|
+
* directory literally named `.`, so every deployed key landed one level away
|
|
70
|
+
* from where the config said it did. The visible cost was that the conditioned
|
|
71
|
+
* IAM binding `deployments/gcp/provision.ts` writes — which grants writes under
|
|
72
|
+
* `objects/data/` — matched nothing, and the first revision 403'd on its boot
|
|
73
|
+
* reconcile. A relative path is relative without being spelled that way.
|
|
74
|
+
*/
|
|
75
|
+
export function profileDir(profile: string): string {
|
|
76
|
+
return `${DATA_DIR}/${profile}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const layout = {
|
|
80
|
+
/**
|
|
81
|
+
* Connections, provider state, and cursors: one object per key.
|
|
82
|
+
*
|
|
83
|
+
* The dot is load-bearing, exactly as it is for `audit` below — a provider
|
|
84
|
+
* is namespaced `<provider>/<connection>` under `blobs`, and a provider id
|
|
85
|
+
* is `[a-z][a-z0-9_]*`, so a name carrying a dot is one no provider can be
|
|
86
|
+
* scoped into.
|
|
87
|
+
*/
|
|
88
|
+
state: (profile: string): string => `${profileDir(profile)}/state.kv`,
|
|
89
|
+
/** System credentials — OAuth tokens, the profile token. Never reachable from MCP. */
|
|
90
|
+
credentials: (profile: string): string => `${profileDir(profile)}/credentials.enc`,
|
|
91
|
+
/** The owner's own items, under their own key. */
|
|
92
|
+
vault: (profile: string): string => `${profileDir(profile)}/vault.enc`,
|
|
93
|
+
/**
|
|
94
|
+
* This profile's skills — `<name>.md` or `<name>/SKILL.md`, either layout.
|
|
95
|
+
*
|
|
96
|
+
* The dot carries the same weight it does for `state` and `audit`, and here
|
|
97
|
+
* it is not hypothetical: `skills` is a real provider id, so `skills/` under
|
|
98
|
+
* the blob root is the prefix its own connection blobs would scope into.
|
|
99
|
+
*/
|
|
100
|
+
skills: (profile: string): string => `${profileDir(profile)}/skills.d`,
|
|
101
|
+
/**
|
|
102
|
+
* The provider manifests this profile declares.
|
|
103
|
+
*
|
|
104
|
+
* Per profile for the same reason a connection is. A manifest names a host,
|
|
105
|
+
* an OpenAPI document, and the credential refs that reach them — which is a
|
|
106
|
+
* description of somebody's infrastructure, and work's is not personal's to
|
|
107
|
+
* read.
|
|
108
|
+
*/
|
|
109
|
+
providers: (profile: string): string => `${profileDir(profile)}/providers.d`,
|
|
110
|
+
/** The blob root every provider is namespaced under. */
|
|
111
|
+
blobs: (profile: string): string => profileDir(profile),
|
|
112
|
+
/**
|
|
113
|
+
* The audit log: one object per event, under the profile's blob root.
|
|
114
|
+
*
|
|
115
|
+
* The dot is doing real work. A provider is namespaced to
|
|
116
|
+
* `<provider>/<connection>` under `blobs` above, and a provider id is
|
|
117
|
+
* `[a-z][a-z0-9_]*` — so a name carrying a dot is one no provider can be
|
|
118
|
+
* scoped to. Without it, a provider called `audit` would be handed a store
|
|
119
|
+
* rooted inside the log, which is a hole in ADR-007's wall rather than an
|
|
120
|
+
* untidy filename.
|
|
121
|
+
*/
|
|
122
|
+
audit: (profile: string): string => `${profileDir(profile)}/audit.log`,
|
|
123
|
+
} as const;
|