@lanes-sh/link 0.7.2 → 0.9.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/README.md +20 -10
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +148 -73
- package/package.json +2 -1
- package/src/audit/index.ts +8 -1
- package/src/auth/index.ts +58 -2
- package/src/auth/lanes/assertion.ts +256 -0
- package/src/auth/lanes/callback.ts +135 -0
- package/src/auth/lanes/federation.ts +50 -0
- package/src/auth/lanes/login.ts +294 -0
- package/src/auth/lanes/members.ts +103 -0
- package/src/auth/lanes/session.ts +97 -0
- package/src/auth/oauth/grant.ts +183 -0
- package/src/auth/oauth/result.ts +27 -0
- package/src/auth/oauth/server.ts +176 -203
- package/src/auth/oauth/store.ts +85 -4
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +109 -0
- package/src/cli/argv.ts +57 -3
- package/src/cli/audit-change.ts +140 -0
- package/src/cli/callback-page.ts +36 -115
- package/src/cli/commands/auth-dispatch.ts +48 -0
- package/src/cli/commands/auth.ts +229 -0
- package/src/cli/commands/connect/accounts.ts +4 -4
- package/src/cli/commands/connect/authorise.ts +4 -4
- package/src/cli/commands/connect/bind-credential.ts +2 -1
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/custom/write.ts +2 -2
- package/src/cli/commands/connect/grant.ts +29 -14
- package/src/cli/commands/connect/index.ts +90 -88
- package/src/cli/commands/connect/options.ts +83 -0
- package/src/cli/commands/connect/registration.ts +50 -0
- package/src/cli/commands/connect/requirements.ts +1 -1
- package/src/cli/commands/connect/settle.ts +16 -6
- package/src/cli/commands/connect/target-note.ts +7 -2
- package/src/cli/commands/connect/unknown.ts +1 -1
- package/src/cli/commands/connect/variables.ts +3 -2
- package/src/cli/commands/connection-list.ts +116 -0
- package/src/cli/commands/connection.ts +183 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +24 -12
- package/src/cli/commands/knowledge/index.ts +49 -89
- package/src/cli/commands/knowledge/migrate.ts +79 -13
- package/src/cli/commands/knowledge/show.ts +97 -0
- package/src/cli/commands/knowledge.ts +2 -1
- package/src/cli/commands/mcp/harnesses.ts +30 -8
- package/src/cli/commands/mcp/onboarding.ts +86 -0
- package/src/cli/commands/mcp/register.ts +16 -2
- package/src/cli/commands/mcp.ts +1 -0
- package/src/cli/commands/members.ts +288 -0
- package/src/cli/commands/operate/attach.ts +3 -3
- package/src/cli/commands/operate/audit.ts +11 -7
- package/src/cli/commands/operate/auth.ts +28 -11
- package/src/cli/commands/operate/findings.ts +2 -1
- package/src/cli/commands/operate/inspect.ts +37 -19
- package/src/cli/commands/operate/migrate.ts +33 -13
- package/src/cli/commands/operate/outputs.ts +3 -3
- package/src/cli/commands/operate/pair-certificate.ts +141 -0
- package/src/cli/commands/operate/pair.ts +324 -0
- package/src/cli/commands/operate/policy.ts +73 -22
- package/src/cli/commands/operate/serve.ts +53 -5
- package/src/cli/commands/operate/status.ts +18 -10
- package/src/cli/commands/operate/tools.ts +2 -2
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/owner/assets.ts +2 -2
- package/src/cli/commands/owner/entities.ts +2 -2
- package/src/cli/commands/owner/memory.ts +2 -2
- package/src/cli/commands/owner/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/owner/tasks.ts +2 -2
- package/src/cli/commands/owner/vault.ts +3 -3
- package/src/cli/commands/profile/disposition.ts +236 -0
- package/src/cli/commands/profile/removal.ts +154 -64
- package/src/cli/commands/profile/remove.ts +83 -7
- package/src/cli/commands/profile.ts +79 -16
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +39 -17
- package/src/cli/commands/set-workspace.ts +96 -0
- package/src/cli/commands/setup.ts +2 -2
- package/src/cli/commands/sync.ts +8 -8
- package/src/cli/commands/target.ts +9 -7
- package/src/cli/commands/update-migration.ts +54 -0
- package/src/cli/commands/update.ts +78 -24
- package/src/cli/config-edit.ts +99 -143
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair-sweep.ts +119 -0
- package/src/cli/config-repair.ts +131 -125
- package/src/cli/config-templates.ts +200 -0
- package/src/cli/contract3-credentials.ts +294 -0
- package/src/cli/contract3-data.ts +262 -0
- package/src/cli/contract3-layout.ts +46 -0
- package/src/cli/contract3-shape.ts +212 -0
- package/src/cli/contract3.ts +399 -0
- package/src/cli/contract4-credentials.ts +207 -0
- package/src/cli/contract4-data.ts +399 -0
- package/src/cli/contract4-rename.ts +73 -0
- package/src/cli/contract4-yaml.ts +223 -0
- package/src/cli/contract4.ts +342 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/identity.ts +44 -26
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +94 -14
- package/src/cli/migrate-move.ts +166 -0
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +6 -7
- package/src/cli/runtime/open.ts +64 -99
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/select.ts +2 -11
- package/src/cli/runtime/stores.ts +58 -0
- package/src/cli/runtime/types.ts +106 -0
- package/src/cli/runtime/vault.ts +19 -4
- package/src/cli/runtime/workspace.ts +60 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection-require.ts +44 -13
- package/src/cli/selection.ts +127 -145
- package/src/cli/usage.ts +40 -20
- package/src/cli/workspace-migrate.ts +152 -22
- package/src/connectivity/manifest/provider.ts +34 -13
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/connectivity/transports/imap/parser.ts +70 -9
- package/src/deployments/adapters/filesystem.ts +18 -3
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +38 -29
- package/src/deployments/gcp/bucket.ts +58 -11
- package/src/deployments/gcp/provision.ts +7 -7
- package/src/deployments/knowledge.ts +9 -4
- package/src/deployments/prepare.ts +72 -24
- package/src/deployments/record.ts +1 -1
- package/src/deployments/report.ts +2 -2
- package/src/deployments/serving.ts +15 -74
- package/src/deployments/target.ts +34 -13
- package/src/deployments/upload.ts +60 -27
- package/src/dispatch/deps.ts +88 -0
- package/src/dispatch/dispatch.ts +21 -62
- package/src/policy/index.ts +47 -15
- package/src/profile/connections.ts +195 -0
- package/src/profile/deployments.ts +86 -8
- package/src/profile/index.ts +35 -6
- package/src/profile/knowledge.ts +18 -5
- package/src/profile/layout.ts +163 -90
- package/src/profile/load.ts +133 -64
- package/src/profile/pairing.ts +32 -0
- package/src/profile/primitives.ts +35 -1
- package/src/profile/registry.ts +6 -6
- package/src/profile/schema.ts +181 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +104 -2
- package/src/profile/workspace.ts +124 -33
- package/src/providers/assets/provider.ts +6 -6
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/entities/provider.ts +6 -6
- package/src/providers/entities/writes.ts +1 -1
- package/src/providers/identity/provider.ts +2 -2
- package/src/providers/memory/provider.ts +26 -8
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/setup/provider.ts +3 -3
- package/src/providers/skills/provider.ts +2 -2
- package/src/providers/slack/index.ts +2 -2
- package/src/providers/tasks/provider.ts +6 -6
- package/src/providers/vault/provider.ts +1 -1
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/registry/registry.ts +1 -1
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +89 -104
- package/src/server/generation.ts +10 -1
- package/src/server/harness.ts +71 -13
- package/src/server/index.ts +31 -0
- package/src/server/mcp/build.ts +20 -1
- package/src/server/mcp/client-info.ts +54 -0
- package/src/server/mcp/guide.ts +120 -0
- package/src/server/mcp/instructions.ts +22 -22
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/routing.ts +3 -3
- package/src/server/mcp/tools.ts +25 -6
- package/src/server/mcp/visibility.ts +74 -7
- package/src/server/oauth.ts +29 -109
- package/src/server/read/credential.ts +134 -0
- package/src/server/read/deployed.ts +56 -0
- package/src/server/read/listener.ts +54 -0
- package/src/server/read/open.ts +101 -0
- package/src/server/read/routes.ts +247 -0
- package/src/server/read/state.ts +171 -0
- package/src/stores/blobs/conformance.ts +19 -0
- package/src/stores/state/index.ts +76 -10
- package/src/stores/state/testing.ts +5 -1
package/src/profile/layout.ts
CHANGED
|
@@ -1,123 +1,196 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Where a
|
|
2
|
+
* Where a workspace's files live, in one place.
|
|
3
3
|
*
|
|
4
4
|
* ```
|
|
5
5
|
* ~/.lanes-link/
|
|
6
|
-
* ├──
|
|
7
|
-
* ├──
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* ├──
|
|
15
|
-
* ├──
|
|
6
|
+
* ├── workspaces.yaml the workspaces this machine knows, and the default
|
|
7
|
+
* ├── connections.yaml every account authorised in this workspace
|
|
8
|
+
* ├── credentials.enc system credentials, and its .key
|
|
9
|
+
* ├── providers.d/ the operator's own provider manifests
|
|
10
|
+
* ├── audit.log/ one object per event, one chain
|
|
11
|
+
* ├── state.kv/ connections, discovery, the endpoint's own OAuth server
|
|
12
|
+
* └── profiles/
|
|
13
|
+
* └── <profile>/ everything one profile is
|
|
14
|
+
* ├── profile.yaml what it selects, and who may use it
|
|
15
|
+
* ├── state.kv/ cursors, and each provider's own keys
|
|
16
|
+
* ├── vault.d/<id>.enc one sealed document per vault connection
|
|
17
|
+
* ├── skills.d/<id>/ procedures, one <name>/SKILL.md each
|
|
16
18
|
* └── <provider>/<connection>/… whatever that provider stores
|
|
17
19
|
* ```
|
|
18
20
|
*
|
|
19
|
-
* **
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* **A profile is one directory, and it owns its bytes.** Both halves reverse
|
|
22
|
+
* something recent. ADR-059 put the owner layer's bytes beside the *connection*,
|
|
23
|
+
* so a profile owned nothing and two profiles granting `memory.lan1` read one
|
|
24
|
+
* note; ADR-066 makes the profile the container again, so they read two. And the
|
|
25
|
+
* declaration used to sit at `profiles/<name>.yaml` while the data sat under
|
|
26
|
+
* `data/<name>/` — the same structure in two places, for no reason anybody could
|
|
27
|
+
* state (ADR-067).
|
|
25
28
|
*
|
|
26
|
-
* **
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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.
|
|
29
|
+
* **There is no `data/` any more.** It meant "what a deployed revision writes,
|
|
30
|
+
* as against the config it reads", which was worth a directory. `profile.yaml`
|
|
31
|
+
* lives beside the bytes now, so that line has to be drawn inside it regardless
|
|
32
|
+
* — and a directory holding everything names nothing. The IAM grant says which
|
|
33
|
+
* prefixes are writable instead of saying which one is not.
|
|
34
34
|
*
|
|
35
|
-
* **
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* ADR-030 has the argument; ADR-009's "profiles share nothing" is what it
|
|
44
|
-
* restores.
|
|
35
|
+
* **The dot is load-bearing one level down, not here.** The root is not a
|
|
36
|
+
* provider blob root; a *profile's* directory is. A provider is namespaced
|
|
37
|
+
* `<provider>/<connection>` under it and a provider id is `[a-z][a-z0-9_]*`, so
|
|
38
|
+
* a name carrying a dot is one no provider can be scoped into. That is not
|
|
39
|
+
* hypothetical for three of the four names there — `skills`, `vault` and `state`
|
|
40
|
+
* would each otherwise be handed a store rooted inside the thing it is meant to
|
|
41
|
+
* be walled off from, which is a hole in ADR-007's wall rather than an untidy
|
|
42
|
+
* filename. `profile.yaml` carries one for the same reason.
|
|
45
43
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* `vault.d/<id>.enc` rather than `vault/<id>.enc` on the same grounds: the
|
|
45
|
+
* sealed document is not a blob the vault provider serves, and a shared `vault/`
|
|
46
|
+
* prefix would put the ciphertext inside the namespace the provider is given.
|
|
49
47
|
*
|
|
50
|
-
*
|
|
48
|
+
* `audit.log/` and `state.kv/` stay at the workspace. One endpoint serves every
|
|
49
|
+
* profile and every event already records the profile it acted in, so one chain
|
|
50
|
+
* is both sufficient and better evidence than several; and a connection's
|
|
51
|
+
* reconcile status is a fact about an account, which a `connect` in one profile
|
|
52
|
+
* must not have to repeat in the next. What follows the profile is what a
|
|
53
|
+
* profile's use of an account produces — cursors, and the provider's own keys.
|
|
51
54
|
*
|
|
52
|
-
*
|
|
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.
|
|
55
|
+
* These are **defaults**. A workspace that declares its own paths keeps them.
|
|
59
56
|
*/
|
|
60
57
|
|
|
61
|
-
/** The directory
|
|
62
|
-
export const
|
|
58
|
+
/** The directory holding one subdirectory per profile. */
|
|
59
|
+
export const PROFILES_DIR = 'profiles';
|
|
63
60
|
|
|
64
61
|
/**
|
|
65
|
-
*
|
|
62
|
+
* What a profile's declaration is called inside its own directory.
|
|
66
63
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* directory
|
|
70
|
-
*
|
|
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.
|
|
64
|
+
* Named here rather than spelled at the two call sites that need it — the path
|
|
65
|
+
* builder below and `listProfiles`, which matches on it to tell a profile
|
|
66
|
+
* directory from anything else under `profiles/`. Two spellings of one filename
|
|
67
|
+
* is how a listing and a loader come to disagree about what exists.
|
|
74
68
|
*/
|
|
75
|
-
export
|
|
76
|
-
return `${DATA_DIR}/${profile}`;
|
|
77
|
-
}
|
|
69
|
+
export const PROFILE_FILE = 'profile.yaml';
|
|
78
70
|
|
|
71
|
+
/**
|
|
72
|
+
* No leading `./` on any of these.
|
|
73
|
+
*
|
|
74
|
+
* It used to carry one, which `path.resolve` discards and an object key does
|
|
75
|
+
* not: a bucket read `./state.kv/x` as a directory literally named `.`, so
|
|
76
|
+
* every deployed key landed one level away from where the config said it did.
|
|
77
|
+
* The visible cost was that the conditioned IAM binding
|
|
78
|
+
* `deployments/gcp/provision.ts` writes matched nothing, and the first revision
|
|
79
|
+
* 403'd on its boot reconcile. A relative path is relative without being
|
|
80
|
+
* spelled that way.
|
|
81
|
+
*/
|
|
79
82
|
export const layout = {
|
|
80
83
|
/**
|
|
81
|
-
*
|
|
84
|
+
* System credentials — OAuth refresh tokens, the CI token. Never reachable
|
|
85
|
+
* from MCP.
|
|
86
|
+
*
|
|
87
|
+
* One store for the workspace, where there used to be one per profile. That
|
|
88
|
+
* is what makes a `credential_ref` unique by construction and retires
|
|
89
|
+
* `collidingRefs`, the deploy preflight that existed because two profiles
|
|
90
|
+
* deployed into one project shared this namespace and the collision was
|
|
91
|
+
* "silent until one profile is reading the other's account" (ADR-043,
|
|
92
|
+
* ADR-057).
|
|
93
|
+
*/
|
|
94
|
+
credentials: (): string => 'credentials.enc',
|
|
95
|
+
/**
|
|
96
|
+
* The provider manifests this workspace declares.
|
|
97
|
+
*
|
|
98
|
+
* Workspace-level, where ADR-030 put them in the profile. A manifest names a
|
|
99
|
+
* host, an OpenAPI document, and the credential refs that reach them — which
|
|
100
|
+
* is to say it *defines a connection*, and connections do not live in a
|
|
101
|
+
* profile any more (ADR-057).
|
|
102
|
+
*/
|
|
103
|
+
providers: (): string => 'providers.d',
|
|
104
|
+
/**
|
|
105
|
+
* The audit log: one object per event, one chain for the workspace.
|
|
106
|
+
*
|
|
107
|
+
* One chain rather than one per profile, because one endpoint serves them all
|
|
108
|
+
* (ADR-009) and every event already records the profile it acted in. It is
|
|
109
|
+
* also what lets `audit tail` filter where it used to select, and what gives
|
|
110
|
+
* the dashboard a single log to read.
|
|
111
|
+
*/
|
|
112
|
+
audit: (): string => 'audit.log',
|
|
113
|
+
/**
|
|
114
|
+
* What the workspace knows about its accounts, and about itself.
|
|
82
115
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
116
|
+
* Connection records and the discovery cache, because both are facts about an
|
|
117
|
+
* account or a provider rather than about anybody's selection of it — a
|
|
118
|
+
* `connect` run once must read as connected from every profile. And the
|
|
119
|
+
* endpoint's own OAuth authorization-server state, which is not about a
|
|
120
|
+
* connection at all: it is the clients that have signed in *to* this endpoint,
|
|
121
|
+
* and there is one endpoint.
|
|
87
122
|
*/
|
|
88
|
-
state: (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
state: (): string => 'state.kv',
|
|
124
|
+
|
|
125
|
+
/** Every profile's directory sits under here. */
|
|
126
|
+
profilesRoot: (): string => PROFILES_DIR,
|
|
127
|
+
/** Everything one profile is. Also the blob root its providers are scoped under. */
|
|
128
|
+
profileDir: (profile: string): string => `${PROFILES_DIR}/${profile}`,
|
|
93
129
|
/**
|
|
94
|
-
*
|
|
130
|
+
* What this profile selects, and who may use it.
|
|
95
131
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
132
|
+
* Inside the profile's own directory, which is the whole of ADR-067 — but it
|
|
133
|
+
* is therefore inside the tree a running endpoint writes, so the IAM condition
|
|
134
|
+
* carves it back out. ADR-007 says a deployed revision never mutates its own
|
|
135
|
+
* configuration, and that rule is older than where the file sits.
|
|
99
136
|
*/
|
|
100
|
-
|
|
137
|
+
profileConfig: (profile: string): string => `${PROFILES_DIR}/${profile}/${PROFILE_FILE}`,
|
|
138
|
+
/** Cursors, and each provider's own keys, for this profile's use of an account. */
|
|
139
|
+
profileState: (profile: string): string => `${PROFILES_DIR}/${profile}/state.kv`,
|
|
140
|
+
/** Every vault connection's sealed document, for this profile. */
|
|
141
|
+
vaultRoot: (profile: string): string => `${PROFILES_DIR}/${profile}/vault.d`,
|
|
142
|
+
/** One sealed document per vault connection, each under its own key. */
|
|
143
|
+
vault: (profile: string, connection: string): string =>
|
|
144
|
+
`${PROFILES_DIR}/${profile}/vault.d/${connection}.enc`,
|
|
101
145
|
/**
|
|
102
|
-
* The
|
|
146
|
+
* The same document, keyed relative to the blob store.
|
|
103
147
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
148
|
+
* `blobs()` is already rooted at the profile's directory, so a blob adapter
|
|
149
|
+
* handed the path above would write `profiles/p/profiles/p/vault.d/...`. Two
|
|
150
|
+
* spellings of one location is exactly what this file exists to prevent, so
|
|
151
|
+
* the second one lives here beside the first rather than being assembled at
|
|
152
|
+
* the call site.
|
|
108
153
|
*/
|
|
109
|
-
|
|
110
|
-
/** The blob root every provider is namespaced under. */
|
|
111
|
-
blobs: (profile: string): string => profileDir(profile),
|
|
154
|
+
vaultKey: (connection: string): string => `vault.d/${connection}.enc`,
|
|
112
155
|
/**
|
|
113
|
-
*
|
|
156
|
+
* Every skills connection's procedures, for this profile.
|
|
114
157
|
*
|
|
115
|
-
* The
|
|
116
|
-
*
|
|
117
|
-
* `
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* untidy filename.
|
|
158
|
+
* The root is exported beside the per-connection path because `deploy` needs
|
|
159
|
+
* to recognise the *area* without knowing which connections exist — and
|
|
160
|
+
* `upload.ts` composes its allowlist back out of these rather than comparing
|
|
161
|
+
* against literals, so a renamed directory moves the store and the thing that
|
|
162
|
+
* sends it together, or neither.
|
|
121
163
|
*/
|
|
122
|
-
|
|
164
|
+
skillsRoot: (profile: string): string => `${PROFILES_DIR}/${profile}/skills.d`,
|
|
165
|
+
/** One skills connection's procedures — `<name>.md` or `<name>/SKILL.md`, either layout. */
|
|
166
|
+
skills: (profile: string, connection: string): string =>
|
|
167
|
+
`${PROFILES_DIR}/${profile}/skills.d/${connection}`,
|
|
168
|
+
/** The blob root this profile's providers are namespaced under. */
|
|
169
|
+
blobs: (profile: string): string => `${PROFILES_DIR}/${profile}`,
|
|
123
170
|
} as const;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* `data/`, which no longer exists — kept for the migrations that address it.
|
|
174
|
+
*
|
|
175
|
+
* Contracts 1 through 3 put everything a profile owned under `data/<profile>/`,
|
|
176
|
+
* and contract 4 moves it out (ADR-067). A migration reads the layout it is
|
|
177
|
+
* migrating *from*, and that layout is frozen: asking `layout` above would
|
|
178
|
+
* compare a contract-3 path against a contract-4 default and match nothing.
|
|
179
|
+
* `migrate-plan.ts` already spells the contract-1 defaults out for the same
|
|
180
|
+
* reason. Nothing outside a migration should import this.
|
|
181
|
+
*/
|
|
182
|
+
export const LEGACY_DATA_DIR = 'data';
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The registry's name, and a profile's path, through contract 3.
|
|
186
|
+
*
|
|
187
|
+
* Recognised, never written. Two live functions answer to these — the ancestor
|
|
188
|
+
* walk and `listProfiles` — for one reason: a workspace that needs migrating
|
|
189
|
+
* has to be findable by the command that migrates it, or `doctor --fix` reports
|
|
190
|
+
* no workspace here and the operator has a wall with no door.
|
|
191
|
+
*/
|
|
192
|
+
export const LEGACY_WORKSPACE_FILE = 'lanes-link.yaml';
|
|
193
|
+
|
|
194
|
+
export function legacyProfileConfig(profile: string): string {
|
|
195
|
+
return `${PROFILES_DIR}/${profile}.yaml`;
|
|
196
|
+
}
|
package/src/profile/load.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parse as parseYaml } from 'yaml';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import {
|
|
4
|
+
SINGLE_INSTANCE_PROVIDERS,
|
|
4
5
|
SUPPORTED_CONTRACT,
|
|
5
6
|
configSchema,
|
|
6
7
|
type Config,
|
|
7
|
-
type PolicyRuleConfig,
|
|
8
8
|
} from './schema.ts';
|
|
9
9
|
import { findSecrets, formatSecretFindings } from './secret-detection.ts';
|
|
10
10
|
|
|
@@ -21,7 +21,7 @@ export class ConfigError extends Error {
|
|
|
21
21
|
export interface LoadedConfig {
|
|
22
22
|
readonly config: Config;
|
|
23
23
|
readonly source: string;
|
|
24
|
-
/** `provider.id` for every
|
|
24
|
+
/** `provider.id` for every connection this profile grants, in declaration order. */
|
|
25
25
|
readonly connectionKeys: readonly string[];
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -39,8 +39,12 @@ export interface LoadedConfig {
|
|
|
39
39
|
* 3. Schema shape.
|
|
40
40
|
* 4. Referential integrity, which needs a well-formed document to check.
|
|
41
41
|
*/
|
|
42
|
-
export function validateConfig(
|
|
43
|
-
|
|
42
|
+
export function validateConfig(
|
|
43
|
+
raw: unknown,
|
|
44
|
+
source = '<config>',
|
|
45
|
+
contract = SUPPORTED_CONTRACT,
|
|
46
|
+
): Config {
|
|
47
|
+
const config = validateConfigShape(raw, source, contract);
|
|
44
48
|
assertReferentialIntegrity(config, source);
|
|
45
49
|
return config;
|
|
46
50
|
}
|
|
@@ -58,12 +62,16 @@ export function validateConfig(raw: unknown, source = '<config>'): Config {
|
|
|
58
62
|
* through `validateConfig`, and the split exists so that the one command whose
|
|
59
63
|
* job is to fix a refusal is not blocked by it.
|
|
60
64
|
*/
|
|
61
|
-
export function validateConfigShape(
|
|
65
|
+
export function validateConfigShape(
|
|
66
|
+
raw: unknown,
|
|
67
|
+
source = '<config>',
|
|
68
|
+
contract = SUPPORTED_CONTRACT,
|
|
69
|
+
): Config {
|
|
62
70
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
63
71
|
throw new ConfigError(`${source}: expected a YAML mapping at the top level`);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
assertSupportedContract(raw, source);
|
|
74
|
+
assertSupportedContract(raw, source, contract);
|
|
67
75
|
|
|
68
76
|
const secrets = findSecrets(raw);
|
|
69
77
|
if (secrets.length > 0) {
|
|
@@ -88,7 +96,17 @@ export function validateConfigShape(raw: unknown, source = '<config>'): Config {
|
|
|
88
96
|
* risks reading a document as more permissive than the operator wrote it, and
|
|
89
97
|
* refusing to start is always the safer failure.
|
|
90
98
|
*/
|
|
91
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The contract this document must declare.
|
|
101
|
+
*
|
|
102
|
+
* `expected` is `SUPPORTED_CONTRACT` everywhere except inside a migration,
|
|
103
|
+
* which writes intermediate shapes on the way to the newest one — contract 1 to
|
|
104
|
+
* 2 to 3 to 4, saving at each step. Refusing those would mean a migration
|
|
105
|
+
* cannot use `ConfigDocument` at all and has to write YAML past the validator,
|
|
106
|
+
* losing the secret-shaped-value check that is the reason the validator runs on
|
|
107
|
+
* every save. A step that declares which contract it is producing keeps both.
|
|
108
|
+
*/
|
|
109
|
+
function assertSupportedContract(raw: object, source: string, expected: number): void {
|
|
92
110
|
const contract = (raw as { contract?: unknown }).contract;
|
|
93
111
|
|
|
94
112
|
if (typeof contract !== 'number' || !Number.isInteger(contract)) {
|
|
@@ -97,14 +115,20 @@ function assertSupportedContract(raw: object, source: string): void {
|
|
|
97
115
|
);
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
if (contract !==
|
|
101
|
-
const direction = contract >
|
|
118
|
+
if (contract !== expected) {
|
|
119
|
+
const direction = contract > expected ? 'newer than' : 'older than';
|
|
102
120
|
throw new ConfigError(
|
|
103
121
|
`${source}: contract ${contract} is ${direction} the contract this binary implements (${SUPPORTED_CONTRACT}). ` +
|
|
104
122
|
`Refusing to load rather than guessing at what the document means. ` +
|
|
123
|
+
// **Naming the command is the whole point of this branch.** ADR-051
|
|
124
|
+
// records what a refusal that names none costs: `validateConfig` runs
|
|
125
|
+
// from every command, so one stale workspace takes `status`, `start`,
|
|
126
|
+
// `plan` and `doctor` down together, and "migrate the config" left an
|
|
127
|
+
// operator with a wall and no door. `doctor --fix` runs every migration
|
|
128
|
+
// between the contract on disk and this one.
|
|
105
129
|
(contract > SUPPORTED_CONTRACT
|
|
106
130
|
? 'Upgrade lanes-link.'
|
|
107
|
-
: 'Migrate
|
|
131
|
+
: 'Migrate it with: lanes link doctor --fix'),
|
|
108
132
|
);
|
|
109
133
|
}
|
|
110
134
|
}
|
|
@@ -166,8 +190,11 @@ function formatZodIssues(error: z.ZodError): string {
|
|
|
166
190
|
export interface ProviderRename {
|
|
167
191
|
/** What a row naming the old id should say instead. */
|
|
168
192
|
readonly to: string;
|
|
169
|
-
/**
|
|
170
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The account label that meant this row was the built-in rather than a
|
|
195
|
+
* vendor's, or `null` where the id belongs to nobody now.
|
|
196
|
+
*/
|
|
197
|
+
readonly keeps: string | null;
|
|
171
198
|
/** What the plain noun now names, for the sentence below. */
|
|
172
199
|
readonly becomes: string;
|
|
173
200
|
/** What it used to name. */
|
|
@@ -190,8 +217,12 @@ export interface ProviderRename {
|
|
|
190
217
|
export const RENAMED_PROVIDERS: Readonly<Record<string, ProviderRename>> = {
|
|
191
218
|
tasks: {
|
|
192
219
|
to: 'google_tasks',
|
|
193
|
-
|
|
194
|
-
|
|
220
|
+
// **`null`, where this was `'Tasks'`.** The refusal existed because the
|
|
221
|
+
// built-in claimed `tasks`, so a stale Google Tasks row rebound to it and
|
|
222
|
+
// the label was the only evidence of which was meant (ADR-051). The
|
|
223
|
+
// built-in is `lanes_tasks` now, so no label makes a row here legitimate.
|
|
224
|
+
keeps: null,
|
|
225
|
+
becomes: 'nobody\u2019s provider id \u2014 the built-in task list is lanes_tasks',
|
|
195
226
|
was: 'Google Tasks',
|
|
196
227
|
noun: 'task list',
|
|
197
228
|
},
|
|
@@ -208,7 +239,7 @@ export const RENAMED_PROVIDERS: Readonly<Record<string, ProviderRename>> = {
|
|
|
208
239
|
* and whoever is reading this refusal just typed which one.
|
|
209
240
|
*/
|
|
210
241
|
function repairCommand(config: Config): string {
|
|
211
|
-
return `lanes link doctor --fix --profile ${config.instance.profile} --
|
|
242
|
+
return `lanes link doctor --fix --profile ${config.instance.profile} --workspace <name>`;
|
|
212
243
|
}
|
|
213
244
|
|
|
214
245
|
/** The rename a row is owed, or `null` when it is owed none. */
|
|
@@ -217,11 +248,12 @@ export function renamedProviderFor(connection: {
|
|
|
217
248
|
account: string;
|
|
218
249
|
}): ProviderRename | null {
|
|
219
250
|
const moved = RENAMED_PROVIDERS[connection.provider];
|
|
220
|
-
if (!moved
|
|
221
|
-
|
|
251
|
+
if (!moved) return null;
|
|
252
|
+
// `keeps === null` means the id belongs to nobody, so no label exempts a row.
|
|
253
|
+
return moved.keeps !== null && connection.account === moved.keeps ? null : moved;
|
|
222
254
|
}
|
|
223
255
|
|
|
224
|
-
function
|
|
256
|
+
export function describeRename(
|
|
225
257
|
connection: { provider: string; account: string },
|
|
226
258
|
repair: string,
|
|
227
259
|
): string | null {
|
|
@@ -229,11 +261,15 @@ function renamedProvider(
|
|
|
229
261
|
if (!moved) return null;
|
|
230
262
|
|
|
231
263
|
return (
|
|
232
|
-
|
|
233
|
-
|
|
264
|
+
(moved.keeps === null
|
|
265
|
+
? `"${connection.provider}" is ${moved.becomes}.\n`
|
|
266
|
+
: `"${connection.provider}" is now ${moved.becomes}, and this row is labelled ` +
|
|
267
|
+
`"${connection.account}" rather than "${moved.keeps}".\n`) +
|
|
234
268
|
` If it was ${moved.was}: set provider to ${moved.to} here, and rename any ` +
|
|
235
269
|
`"${connection.provider}.*" policy rule.\n` +
|
|
236
|
-
|
|
270
|
+
(moved.keeps === null
|
|
271
|
+
? ` If it is your own ${moved.noun}: it is lanes_${connection.provider} now.\n`
|
|
272
|
+
: ` If it is your own ${moved.noun}: set account to ${moved.keeps}.\n`) +
|
|
237
273
|
` ${repair} applies the first, where a stored credential proves it.`
|
|
238
274
|
);
|
|
239
275
|
}
|
|
@@ -241,30 +277,74 @@ function renamedProvider(
|
|
|
241
277
|
function assertReferentialIntegrity(config: Config, source: string): void {
|
|
242
278
|
const problems: string[] = [];
|
|
243
279
|
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
// `
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
// Nothing about targets is checked here any more. A profile declares none
|
|
281
|
+
// (ADR-052): the workspace holding this file declares the one it lives in.
|
|
282
|
+
//
|
|
283
|
+
// Nothing about *connections* is either, and that is newer. A connection
|
|
284
|
+
// belongs to the workspace (ADR-057), so whether a grant names a real one is
|
|
285
|
+
// a question this file cannot answer on its own — `assertGrantsResolve` in
|
|
286
|
+
// `./connections.ts` answers it once `connections.yaml` has been read. What
|
|
287
|
+
// is checkable from one profile alone is checked here, and only that.
|
|
288
|
+
|
|
289
|
+
// A grant names one connection, so every capability in it belongs to that
|
|
290
|
+
// connection's provider. Worth refusing rather than warning: `allowedConnections`
|
|
291
|
+
// filters candidates to the capability's own provider before policy is
|
|
292
|
+
// consulted, so `allow: [calendar.*]` on a row granting `gmail.personal`
|
|
293
|
+
// matches nothing, ever, while reading exactly like a grant that works.
|
|
294
|
+
const grantKeys = new Set<string>();
|
|
295
|
+
config.grants.forEach((grant, index) => {
|
|
296
|
+
if (grantKeys.has(grant.connection)) {
|
|
297
|
+
problems.push(`grants[${index}]: duplicate grant for "${grant.connection}"`);
|
|
258
298
|
}
|
|
259
|
-
|
|
299
|
+
grantKeys.add(grant.connection);
|
|
300
|
+
|
|
301
|
+
const provider = grant.connection.split('.')[0] ?? '';
|
|
302
|
+
|
|
303
|
+
// The renamed-provider check is not here any more. It compares a row's
|
|
304
|
+
// *account* against the label the built-in keeps — "is this really Google
|
|
305
|
+
// Tasks?" — and an account lives in `connections.yaml` now (ADR-057). A
|
|
306
|
+
// profile grant carries only the key, so asking here would either need the
|
|
307
|
+
// other file or answer from nothing; `assertNoRenamedProviders` asks where
|
|
308
|
+
// the answer is.
|
|
309
|
+
|
|
310
|
+
for (const [field, rules] of [
|
|
311
|
+
['allow', grant.allow],
|
|
312
|
+
['deny', grant.deny],
|
|
313
|
+
] as const) {
|
|
314
|
+
rules.forEach((rule, ruleIndex) => {
|
|
315
|
+
if (rule.capability === '*') return;
|
|
316
|
+
const named = rule.capability.split('.')[0] ?? '';
|
|
317
|
+
if (named === provider) return;
|
|
260
318
|
|
|
261
|
-
|
|
262
|
-
|
|
319
|
+
problems.push(
|
|
320
|
+
`grants[${index}].${field}[${ruleIndex}]: "${rule.capability}" names provider ` +
|
|
321
|
+
`"${named}", but this row grants "${grant.connection}". A rule here governs ` +
|
|
322
|
+
`that connection and nothing else, so it can only name "${provider}.*".`,
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
263
326
|
});
|
|
264
327
|
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
328
|
+
// At most one skills grant and one vault grant, for the reason
|
|
329
|
+
// `SINGLE_INSTANCE_PROVIDERS` gives: both surface as flat names with no
|
|
330
|
+
// argument to route on, so a second instance is a collision rather than a
|
|
331
|
+
// choice.
|
|
332
|
+
for (const provider of SINGLE_INSTANCE_PROVIDERS) {
|
|
333
|
+
const granted = config.grants
|
|
334
|
+
.map((grant) => grant.connection)
|
|
335
|
+
.filter((ref) => ref.startsWith(`${provider}.`));
|
|
336
|
+
|
|
337
|
+
if (granted.length > 1) {
|
|
338
|
+
problems.push(
|
|
339
|
+
`grants: ${granted.join(' and ')} — a profile may grant one "${provider}" connection. ` +
|
|
340
|
+
`It is surfaced by name with nothing to route on, so two would be one name for two things.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Same reason as a duplicate grant: two entries with the same kind and value
|
|
346
|
+
// cannot both be meant, and the one that loses is invisible. It matters more
|
|
347
|
+
// here than it looks, because the two would usually differ only in their
|
|
268
348
|
// `note` — so the discarded one is precisely the guidance someone wrote down
|
|
269
349
|
// to stop an agent picking wrong.
|
|
270
350
|
const identityKeys = new Set<string>();
|
|
@@ -276,28 +356,17 @@ function assertReferentialIntegrity(config: Config, source: string): void {
|
|
|
276
356
|
identityKeys.add(key);
|
|
277
357
|
});
|
|
278
358
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (field === 'allow' && !providerNames.has(providerOfCapability)) {
|
|
291
|
-
problems.push(
|
|
292
|
-
`${where}: "${rule.capability}" names provider "${providerOfCapability}", which has no connection` +
|
|
293
|
-
(providerNames.size > 0 ? ` (have: ${[...providerNames].join(', ')})` : ''),
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
checkRules(config.policy.allow, 'allow');
|
|
300
|
-
checkRules(config.policy.deny, 'deny');
|
|
359
|
+
// A subject listed twice is one row deciding the role and the other doing
|
|
360
|
+
// nothing, and which one wins is iteration order. Since the two would differ
|
|
361
|
+
// only in `role`, the silent loser is exactly the line someone added to
|
|
362
|
+
// promote or demote somebody (ADR-060).
|
|
363
|
+
const subjects = new Set<string>();
|
|
364
|
+
config.members.forEach((member, index) => {
|
|
365
|
+
if (subjects.has(member.subject)) {
|
|
366
|
+
problems.push(`members[${index}]: duplicate subject "${member.subject}"`);
|
|
367
|
+
}
|
|
368
|
+
subjects.add(member.subject);
|
|
369
|
+
});
|
|
301
370
|
|
|
302
371
|
if (problems.length > 0) {
|
|
303
372
|
throw new ConfigError(`${source}:\n${problems.map((p) => ` ${p}`).join('\n')}`, problems);
|
|
@@ -316,7 +385,7 @@ export function parseConfig(text: string, source = '<config>'): LoadedConfig {
|
|
|
316
385
|
return {
|
|
317
386
|
config,
|
|
318
387
|
source,
|
|
319
|
-
connectionKeys: config.
|
|
388
|
+
connectionKeys: config.grants.map((grant) => grant.connection),
|
|
320
389
|
};
|
|
321
390
|
}
|
|
322
391
|
|