@lanes-sh/link 0.8.0 → 0.9.1
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 +1 -25
- package/instructions/skills/lanes-link/SKILL.md +12 -12
- package/package.json +1 -1
- package/src/auth/oauth/store.ts +21 -5
- package/src/cli/accepts.ts +6 -5
- package/src/cli/brand.ts +20 -15
- package/src/cli/callback-page.ts +8 -7
- package/src/cli/commands/connect/index.ts +2 -1
- package/src/cli/commands/connect/settle.ts +12 -4
- package/src/cli/commands/connection.ts +2 -1
- package/src/cli/commands/identity.ts +4 -4
- package/src/cli/commands/knowledge/index.ts +9 -16
- package/src/cli/commands/knowledge/migrate.ts +9 -4
- package/src/cli/commands/knowledge/show.ts +14 -9
- package/src/cli/commands/operate/migrate.ts +5 -2
- package/src/cli/commands/operate/pair.ts +1 -1
- package/src/cli/commands/operate/serve.ts +1 -1
- 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/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 +100 -13
- package/src/cli/commands/profile/remove.ts +67 -6
- package/src/cli/commands/profile.ts +33 -6
- package/src/cli/commands/secrets.ts +4 -4
- package/src/cli/commands/update-migration.ts +54 -0
- package/src/cli/commands/update.ts +36 -23
- package/src/cli/config-edit.ts +29 -8
- package/src/cli/config-repair-sweep.ts +183 -0
- package/src/cli/config-repair.ts +90 -141
- package/src/cli/config-templates.ts +26 -24
- package/src/cli/contract3-credentials.ts +294 -0
- package/src/cli/contract3-data.ts +143 -209
- package/src/cli/contract3-layout.ts +46 -0
- package/src/cli/contract3-shape.ts +34 -8
- package/src/cli/contract3.ts +141 -24
- 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 +349 -0
- package/src/cli/identity.ts +44 -26
- package/src/cli/main.ts +6 -1
- package/src/cli/migrate-move.ts +166 -0
- package/src/cli/migrate-plan.ts +3 -3
- package/src/cli/publish.ts +1 -5
- package/src/cli/runtime/open.ts +5 -5
- package/src/cli/runtime/select.ts +2 -11
- package/src/cli/runtime/stores.ts +16 -11
- package/src/cli/runtime/vault.ts +2 -2
- package/src/cli/usage.ts +5 -1
- package/src/cli/workspace-migrate.ts +32 -11
- package/src/connectivity/manifest/provider.ts +31 -12
- package/src/connectivity/transports/imap/parser.ts +70 -9
- package/src/deployments/adapters/filesystem.ts +18 -3
- package/src/deployments/deploy.ts +5 -5
- package/src/deployments/gcp/bucket.ts +42 -6
- package/src/deployments/knowledge.ts +9 -4
- package/src/deployments/target.ts +28 -7
- package/src/deployments/upload.ts +39 -30
- package/src/profile/connections.ts +13 -1
- package/src/profile/deployments.ts +86 -8
- package/src/profile/index.ts +5 -1
- package/src/profile/knowledge.ts +18 -5
- package/src/profile/layout.ts +147 -71
- package/src/profile/load.ts +53 -17
- package/src/profile/schema.ts +11 -2
- package/src/profile/testing.ts +45 -10
- package/src/profile/workspace.ts +66 -30
- package/src/providers/assets/provider.ts +6 -6
- package/src/providers/entities/provider.ts +6 -6
- package/src/providers/entities/writes.ts +1 -1
- package/src/providers/identity/provider.ts +1 -1
- package/src/providers/memory/provider.ts +6 -6
- package/src/providers/setup/provider.ts +3 -3
- package/src/providers/skills/provider.ts +2 -2
- package/src/providers/tasks/provider.ts +6 -6
- package/src/providers/vault/provider.ts +1 -1
- package/src/registry/registry.ts +1 -1
- package/src/server/endpoint.ts +4 -0
- package/src/server/harness.ts +1 -1
- package/src/server/mcp/instructions.ts +21 -21
- package/src/server/mcp/routing.ts +3 -3
- package/src/server/mcp/tools.ts +16 -3
- package/src/server/mcp/visibility.ts +56 -4
- 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/README.md
CHANGED
|
@@ -12,10 +12,7 @@ that speaks MCP — reaches all of it through that one endpoint. Change your AI
|
|
|
12
12
|
context, because none of it ever lived in the agent. Open source, self-hostable, no vendor in the
|
|
13
13
|
middle of your data.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/images/lanes-link-dark.svg">
|
|
17
|
-
<img alt="Claude, Codex, and Gemini all reach one Lanes Link endpoint, which you run yourself. Beneath it sit the profiles it serves: personal, holding Gmail, Memory, and Skills; and work, holding Gmail, Calendar, and Docs." src="docs/images/lanes-link-light.svg">
|
|
18
|
-
</picture>
|
|
15
|
+

|
|
19
16
|
|
|
20
17
|
## Why
|
|
21
18
|
|
|
@@ -81,27 +78,6 @@ tool and worth stating plainly; what it is not is a dependency per request. The
|
|
|
81
78
|
to sign in and to refresh, and a machine offline for a day keeps serving. `lanes link token
|
|
82
79
|
show` still mints a static token for CI, which has no browser to sign in with.
|
|
83
80
|
|
|
84
|
-
## In the Lanes desktop app
|
|
85
|
-
|
|
86
|
-
Prefer not to use a terminal? The [Lanes desktop app](https://lanes.sh/desktop) drives this CLI from
|
|
87
|
-
a settings page. **Settings → Integrations → Lanes Link** installs it, holds the profile and
|
|
88
|
-
workspace every command runs against, starts and stops the endpoint, and registers it with Claude
|
|
89
|
-
Code or Codex. From 0.8.0 your connections, profiles and audit log are on the
|
|
90
|
-
[Lanes dashboard](https://lanes.sh/dashboard/link) instead, which reads your endpoint directly over
|
|
91
|
-
loopback: run `lanes link pair` once to let it.
|
|
92
|
-
|
|
93
|
-

|
|
94
|
-
|
|
95
|
-
It runs the commands above rather than reimplementing them, so consent and the token stay here where
|
|
96
|
-
they belong, and an endpoint set up in the app is the same one you get from a shell. Available from
|
|
97
|
-
Lanes v0.47.0, as a research preview.
|
|
98
|
-
|
|
99
|
-
```console
|
|
100
|
-
$ lanes link desktop # opens the app on that page, installing it if it is not there
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**[How to use it →](https://lanes.sh/docs/desktop/lanes-link)**
|
|
104
|
-
|
|
105
81
|
## What you keep in it
|
|
106
82
|
|
|
107
83
|
Not "what your agent gets" — the distinction is the whole point. These are yours. An agent is a
|
|
@@ -107,7 +107,7 @@ when Y", that is a skill they should write, not a memory entry describing it.
|
|
|
107
107
|
|
|
108
108
|
## Reach for memory before answering from nothing
|
|
109
109
|
|
|
110
|
-
`
|
|
110
|
+
`lanes_memory.search` before concluding you do not know something about this person or
|
|
111
111
|
their work. It is a substring search over their own notes, not a ranked index —
|
|
112
112
|
try more than one wording before deciding it is not there.
|
|
113
113
|
|
|
@@ -118,7 +118,7 @@ entries with `lanes link memory list --profile <name> --workspace <name>` and a
|
|
|
118
118
|
|
|
119
119
|
## Tasks have a status, so finish them rather than deleting them
|
|
120
120
|
|
|
121
|
-
`
|
|
121
|
+
`lanes_tasks.list` answers what is outstanding. It shows `in_progress`, `open` and
|
|
122
122
|
`blocked` and hides the rest, so a listing is what is left to do rather than
|
|
123
123
|
everything that ever was — name a status to see more.
|
|
124
124
|
|
|
@@ -133,7 +133,7 @@ Six of them, and the two that are easy to confuse are worth learning:
|
|
|
133
133
|
| `done` | finished |
|
|
134
134
|
| `dropped` | decided against, which is not the same as finished |
|
|
135
135
|
|
|
136
|
-
**Closing a task is `
|
|
136
|
+
**Closing a task is `lanes_tasks.update` with a status, never `lanes_tasks.remove`.** The
|
|
137
137
|
record of having done it is the useful half, and it is what stops the same thing
|
|
138
138
|
being suggested again next week. Remove is for something recorded by mistake.
|
|
139
139
|
|
|
@@ -180,7 +180,7 @@ value has been printed by accident, say so.
|
|
|
180
180
|
## Who you are writing as is declared, not inferred
|
|
181
181
|
|
|
182
182
|
A profile may declare the names, addresses and handles its owner wants used when
|
|
183
|
-
something is written as them. Call `
|
|
183
|
+
something is written as them. Call `lanes_identity_list` for the profile in play
|
|
184
184
|
before signing a message, choosing an address to send from, or attributing work
|
|
185
185
|
to a handle — **do not** read a name off the conversation, off a previous
|
|
186
186
|
message's signature, or off the account label on a connection. That label is the
|
|
@@ -200,7 +200,7 @@ Nothing you can call writes here, deliberately.
|
|
|
200
200
|
|
|
201
201
|
## Who you are writing *to* is declared as well
|
|
202
202
|
|
|
203
|
-
`
|
|
203
|
+
`lanes_entities_find` holds the people, companies and projects this owner deals with,
|
|
204
204
|
with the addresses and handles to reach each of them. Call it before using
|
|
205
205
|
anyone's address — do not recall one from earlier in the conversation, and do
|
|
206
206
|
not lift one off a message you happen to have read.
|
|
@@ -218,9 +218,9 @@ Where an entity holds two of a kind — a work address and a personal one — th
|
|
|
218
218
|
first is the default and the notes say when to prefer the other, exactly as
|
|
219
219
|
identity works.
|
|
220
220
|
|
|
221
|
-
Writing is a separate grant. Where you have it, `
|
|
222
|
-
and `
|
|
223
|
-
`
|
|
221
|
+
Writing is a separate grant. Where you have it, `lanes_entities_write` declares one
|
|
222
|
+
and `lanes_entities_link` relates two; a field you do not send is left as it is.
|
|
223
|
+
`lanes_entities_forget` does not clean up edges pointing at what it removed, and says
|
|
224
224
|
which ones will dangle.
|
|
225
225
|
|
|
226
226
|
## Attachments are named, not carried
|
|
@@ -279,9 +279,9 @@ shows what was attempted, refusals included.
|
|
|
279
279
|
|
|
280
280
|
## Setting something up
|
|
281
281
|
|
|
282
|
-
Call `
|
|
282
|
+
Call `lanes_setup_overview` before answering any question about what you can reach, and
|
|
283
283
|
before suggesting that anything be connected. It names the accounts reachable in a
|
|
284
|
-
profile and the providers that are not connected yet. `
|
|
284
|
+
profile and the providers that are not connected yet. `lanes_setup_provider` then gives
|
|
285
285
|
one provider's console steps, the values it will ask for, and the exact command.
|
|
286
286
|
|
|
287
287
|
**Memory, tasks, assets, skills and the vault need no setup at all.** They hold
|
|
@@ -306,7 +306,7 @@ lanes link policy allow gmail.users.messages.list --connection gmail.<id> --prof
|
|
|
306
306
|
profiles that grant it, which is the answer when an owner says an account they
|
|
307
307
|
connected is not showing up somewhere.
|
|
308
308
|
|
|
309
|
-
**Take the command from `
|
|
309
|
+
**Take the command from `lanes_setup_provider`; never compose one yourself.** It carries
|
|
310
310
|
the right profile and, where the provider stores a credential per account, the
|
|
311
311
|
`--id` it needs. A command you assembled is one the owner pastes and watches fail.
|
|
312
312
|
|
|
@@ -326,7 +326,7 @@ it needs, print the scopes and let the owner add the flag. Deciding that is thei
|
|
|
326
326
|
|
|
327
327
|
**A new connection is served at once; the tools you were handed are not.**
|
|
328
328
|
Connecting publishes the config to wherever that workspace's endpoint reads it and
|
|
329
|
-
asks the endpoint to re-read it, so a `
|
|
329
|
+
asks the endpoint to re-read it, so a `lanes_setup_overview` straight after connecting
|
|
330
330
|
*does* show the account. What has not changed is the set of tools this session
|
|
331
331
|
was given when it connected — the endpoint does not announce that its tools
|
|
332
332
|
changed, so a capability for a freshly connected account is not callable until
|
package/package.json
CHANGED
package/src/auth/oauth/store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { OAUTH_NAMESPACE, type KeyValueStore } from '#stores/state';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* What an authorization server has to remember.
|
|
@@ -20,10 +20,26 @@ import type { KeyValueStore } from '#stores/state';
|
|
|
20
20
|
* living about a minute.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// Derived from `OAUTH_NAMESPACE`, not spelled. These were `oauth/clients` and
|
|
24
|
+
// the rest while the namespace constant said `oauth.v1`, so the contract-4
|
|
25
|
+
// migration moved every record to a key nothing read — every signed-in client
|
|
26
|
+
// logged out, which is the outcome the migration exists to prevent. Worse, an
|
|
27
|
+
// undotted namespace is not a workspace one, so `isWorkspaceNamespace` sent
|
|
28
|
+
// live records into one profile's store: `profile remove <primary>` would take
|
|
29
|
+
// the endpoint's registrations with it.
|
|
30
|
+
const CLIENTS = `${OAUTH_NAMESPACE}/clients`;
|
|
31
|
+
const CODES = `${OAUTH_NAMESPACE}/codes`;
|
|
32
|
+
const TOKENS = `${OAUTH_NAMESPACE}/tokens`;
|
|
33
|
+
const PENDING = `${OAUTH_NAMESPACE}/pending`;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The four, for the test that holds them to `isWorkspaceNamespace`.
|
|
37
|
+
*
|
|
38
|
+
* Exported rather than retyped there: the pairing between what this module
|
|
39
|
+
* writes and where state routing sends it is the thing that broke, so the test
|
|
40
|
+
* reads the names off the module that owns them.
|
|
41
|
+
*/
|
|
42
|
+
export const OAUTH_SUBSPACES: readonly string[] = [CLIENTS, CODES, TOKENS, PENDING];
|
|
27
43
|
|
|
28
44
|
/** Far above any real number of connectors, and far below a problem. */
|
|
29
45
|
const MAX_CLIENTS = 200;
|
package/src/cli/accepts.ts
CHANGED
|
@@ -39,11 +39,12 @@ export const ACCEPTS: Record<string, readonly string[]> = {
|
|
|
39
39
|
],
|
|
40
40
|
setup: ['id'],
|
|
41
41
|
'profile add': ['workspace', 'non-interactive'],
|
|
42
|
-
// `--
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
42
|
+
// `--delete-data` and `--migrate-to` say what becomes of the bytes the
|
|
43
|
+
// profile owns (ADR-066). Neither is optional in effect: `--yes` skips
|
|
44
|
+
// confirmations of things the command was told to do, and this is a question
|
|
45
|
+
// it has not been asked, so a run with no terminal is refused rather than
|
|
46
|
+
// guessing.
|
|
47
|
+
'profile remove': ['dry-run', 'yes', 'workspace', 'delete-data', 'migrate-to'],
|
|
47
48
|
'profile members': ['me', 'role'],
|
|
48
49
|
disconnect: ['yes', 'keep-credential'],
|
|
49
50
|
// The one repair `doctor` can apply rather than only name. Narrow on purpose:
|
package/src/cli/brand.ts
CHANGED
|
@@ -13,17 +13,19 @@
|
|
|
13
13
|
* Values are transcribed from https://lanes.sh/design/foundations. The rules
|
|
14
14
|
* they express, which the CSS below encodes rather than restates:
|
|
15
15
|
*
|
|
16
|
-
* - **
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* - **Emerald is the only accent.** "Accent for positive, neutral tokens
|
|
17
|
+
* otherwise." A connection that works is `positive`, which is the accent, and
|
|
18
|
+
* everything else is one of two neutrals. `--destructive` is for errors, not
|
|
19
|
+
* for a state that merely needs attention. The accent was gold until emerald
|
|
20
|
+
* replaced it site-wide; there is still only one.
|
|
20
21
|
* - **Two weights, 400 and 500.** Nothing else is available in the loaded faces,
|
|
21
22
|
* so a heavier rule would silently synthesise.
|
|
22
23
|
* - **Lora heads, Geist speaks, Geist Mono for code and eyebrow labels.**
|
|
23
24
|
*
|
|
24
|
-
* `--border
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* `--border` and `--destructive` are constant across themes. The five neutrals
|
|
26
|
+
* swap, and so does `--accent-brand`: the deep emerald goes illegible on the
|
|
27
|
+
* dark ground, so dark mode takes the brighter value. That is why the dark block
|
|
28
|
+
* below is six lines rather than a second stylesheet.
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -61,7 +63,7 @@ export const FONTS =
|
|
|
61
63
|
|
|
62
64
|
/** The one-row footer, identical to the one the Lanes API's own pages carry. */
|
|
63
65
|
export const FOOTER =
|
|
64
|
-
'<div class="footer"><p>
|
|
66
|
+
'<div class="footer"><p>Run many agents at once. Connect them to your tools once - ' +
|
|
65
67
|
'<a href="https://lanes.sh/">lanes.sh</a></p></div>';
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -85,13 +87,13 @@ export const TOKENS = `
|
|
|
85
87
|
--foreground: #171717;
|
|
86
88
|
--muted-foreground: #202329;
|
|
87
89
|
--border: rgba(120,113,108,0.2);
|
|
88
|
-
--accent-
|
|
90
|
+
--accent-brand: #059669;
|
|
89
91
|
--destructive: #A06060;
|
|
90
92
|
/* The two tints the badge variants need. Spelled as rgba rather than
|
|
91
93
|
color-mix because the consent screen opens in whatever browser a phone
|
|
92
94
|
happens to use, and both accents are constant across themes anyway. */
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
+
--brand-fill: rgba(5,150,105,0.1);
|
|
96
|
+
--brand-ring: rgba(5,150,105,0.25);
|
|
95
97
|
--destructive-fill: rgba(160,96,96,0.1);
|
|
96
98
|
--destructive-ring: rgba(160,96,96,0.25);
|
|
97
99
|
--sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -105,6 +107,9 @@ export const TOKENS = `
|
|
|
105
107
|
--muted: #202329;
|
|
106
108
|
--foreground: #EBEAE7;
|
|
107
109
|
--muted-foreground: #A8A29E;
|
|
110
|
+
--accent-brand: #34D399;
|
|
111
|
+
--brand-fill: rgba(52,211,153,0.1);
|
|
112
|
+
--brand-ring: rgba(52,211,153,0.25);
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
* { box-sizing: border-box; }
|
|
@@ -124,21 +129,21 @@ a { color: inherit; }
|
|
|
124
129
|
/* Surfaces are dashed and 10px; controls are 6px; pills are round. */
|
|
125
130
|
.surface { background: var(--card); border: 1px dashed var(--border); border-radius: 10px; }
|
|
126
131
|
|
|
127
|
-
/* The four badge variants, as the design system defines them:
|
|
132
|
+
/* The four badge variants, as the design system defines them: the accent for
|
|
128
133
|
positive, neutral tokens otherwise, destructive for an error only. The ring
|
|
129
134
|
is an inset shadow so it costs no layout, exactly as \`ring-inset\` does. */
|
|
130
135
|
.pill { display: inline-flex; align-items: center; flex: none; white-space: nowrap;
|
|
131
136
|
font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
|
|
132
137
|
padding: 2px 8px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--border); }
|
|
133
|
-
.pill.positive { background: var(--
|
|
134
|
-
box-shadow: inset 0 0 0 1px var(--
|
|
138
|
+
.pill.positive { background: var(--brand-fill); color: var(--accent-brand);
|
|
139
|
+
box-shadow: inset 0 0 0 1px var(--brand-ring); }
|
|
135
140
|
.pill.neutral { background: var(--muted); color: var(--muted-foreground); }
|
|
136
141
|
.pill.quiet { background: transparent; color: var(--muted-foreground); opacity: 0.7; }
|
|
137
142
|
.pill.negative { background: var(--destructive-fill); color: var(--destructive);
|
|
138
143
|
box-shadow: inset 0 0 0 1px var(--destructive-ring); }
|
|
139
144
|
|
|
140
145
|
/* Ghost is the variant every button on these pages is: muted until it is
|
|
141
|
-
wanted, and never
|
|
146
|
+
wanted, and never the accent, which is reserved for saying a thing is good. */
|
|
142
147
|
.btn { font: inherit; font-size: 12px; font-weight: 500; line-height: 1;
|
|
143
148
|
padding: 6px 9px; border-radius: 6px; cursor: pointer;
|
|
144
149
|
background: transparent; color: var(--muted-foreground);
|
package/src/cli/callback-page.ts
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* hardcoded `#0d1117` that read as a black rectangle on a light machine. The
|
|
17
17
|
* system names a background per mode, so the fix is now to paint the right
|
|
18
18
|
* one. `color-scheme` stays, for form controls and scrollbars.
|
|
19
|
-
* - **The success mark
|
|
20
|
-
*
|
|
21
|
-
* integration.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* - **The success mark carries the accent.** It is lucide's `check` at
|
|
20
|
+
* `--accent-brand`, which matches the tick the Lanes app puts against a
|
|
21
|
+
* connected integration. This went the long way round: the mark was `#059669`,
|
|
22
|
+
* was changed to gold because the design system allowed one accent and gold
|
|
23
|
+
* was it, and is emerald again now that emerald is that one accent. The rule
|
|
24
|
+
* never moved, only the colour it points at.
|
|
24
25
|
*
|
|
25
26
|
* What has not changed is that colour is reserved for status and spent nowhere
|
|
26
27
|
* else — not on emphasis, and not on the heading.
|
|
@@ -49,14 +50,14 @@ body { display: flex; align-items: center; justify-content: center;
|
|
|
49
50
|
.label { margin: 0 0 6px; font-size: 15px; font-weight: 500; color: var(--muted-foreground); }
|
|
50
51
|
h1 { margin: 0 0 18px; }
|
|
51
52
|
.detail { margin: 0; font-size: 15px; line-height: 1.6; color: var(--muted-foreground); }
|
|
52
|
-
.icon { display: block; margin: 0 auto 18px; color: var(--accent-
|
|
53
|
+
.icon { display: block; margin: 0 auto 18px; color: var(--accent-brand); }
|
|
53
54
|
.err h1 { color: var(--destructive); }
|
|
54
55
|
`.trim();
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* The success mark, above the label.
|
|
58
59
|
*
|
|
59
|
-
* lucide's `check`, in `--accent-
|
|
60
|
+
* lucide's `check`, in `--accent-brand` - which is what "accent for positive"
|
|
60
61
|
* means when the positive thing is a connection that now works. It carries no
|
|
61
62
|
* colour of its own; `.icon` sets it, so it follows the token if the token moves.
|
|
62
63
|
*
|
|
@@ -35,6 +35,7 @@ import { settleIdentity } from './settle.ts';
|
|
|
35
35
|
import { runStrategySetup } from './strategy.ts';
|
|
36
36
|
import { announceConnectTarget } from './target-note.ts';
|
|
37
37
|
import { unknownProvider } from './unknown.ts';
|
|
38
|
+
import { DISCOVERY_NAMESPACE } from '#stores/state';
|
|
38
39
|
|
|
39
40
|
const PROVISIONAL_ID = 'pending';
|
|
40
41
|
|
|
@@ -265,7 +266,7 @@ export async function runConnect(
|
|
|
265
266
|
credentials: runtime.credentials,
|
|
266
267
|
connectorFor: address.connectorFor,
|
|
267
268
|
remember: async (found) => {
|
|
268
|
-
await runtime.state.kv.set(
|
|
269
|
+
await runtime.state.kv.set(DISCOVERY_NAMESPACE, providerId, JSON.stringify(found));
|
|
269
270
|
registry.setDiscovered(providerId, found);
|
|
270
271
|
},
|
|
271
272
|
});
|
|
@@ -3,7 +3,7 @@ import { bearerTokenAsStored } from '#connectivity/auth/index.ts';
|
|
|
3
3
|
import type { SecretStore } from '#secrets';
|
|
4
4
|
import type { ConnectionConfig, Config } from '#profile';
|
|
5
5
|
import type { AnyConnector, ProviderManifest } from '#connectivity';
|
|
6
|
-
import {
|
|
6
|
+
import { nextConnectionId, resolveAccount } from '../../identity.ts';
|
|
7
7
|
import { style } from '../../output.ts';
|
|
8
8
|
import { PromptCancelled, terminalPrompter, type Prompter } from '../../prompt.ts';
|
|
9
9
|
import { accountSiblings } from './accounts.ts';
|
|
@@ -151,15 +151,23 @@ export async function settleIdentity(input: {
|
|
|
151
151
|
typed = true;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
// Every id in the workspace, not this provider's alone. An id is opaque now,
|
|
155
|
+
// so a reader has nothing but the number to go on and two rows sharing one
|
|
156
|
+
// across providers would be a needless second thing to hold in mind — and
|
|
157
|
+
// `con3` naming exactly one row is what makes it usable in a refusal.
|
|
158
|
+
const taken = runtime.workspaceConnections.map((candidate) => candidate.id);
|
|
155
159
|
|
|
160
|
+
// The reconnect match is on the *account*, which is what tells a repair from
|
|
161
|
+
// a second row. That is the whole reason `resolveAccount` runs before this:
|
|
162
|
+
// without it a failed attempt appends rather than repairs, which is how
|
|
163
|
+
// `Gmail main3` came to exist.
|
|
156
164
|
const connectionId =
|
|
157
165
|
explicitId ??
|
|
158
166
|
(unaccounted
|
|
159
|
-
?
|
|
167
|
+
? nextConnectionId(taken, true)
|
|
160
168
|
: (siblings.find(
|
|
161
169
|
(candidate) => candidate.account.toLowerCase() === account!.toLowerCase(),
|
|
162
|
-
)?.id ??
|
|
170
|
+
)?.id ?? nextConnectionId(taken, false)));
|
|
163
171
|
|
|
164
172
|
return {
|
|
165
173
|
connectionId,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readConnections,
|
|
8
8
|
type ConnectionConfig,
|
|
9
9
|
type Resolution,
|
|
10
|
+
layout,
|
|
10
11
|
} from '#profile';
|
|
11
12
|
import { ConfigDocument } from '../config-edit.ts';
|
|
12
13
|
import { announce, announceWorkspace, emit, ok, print, style, warn } from '../output.ts';
|
|
@@ -230,7 +231,7 @@ export async function removeConnection(
|
|
|
230
231
|
// refused at load, while a connection nothing grants is merely unused. So if
|
|
231
232
|
// the process dies between the two writes, the workspace still opens.
|
|
232
233
|
for (const profile of affected) {
|
|
233
|
-
const document = await ConfigDocument.openKey(root,
|
|
234
|
+
const document = await ConfigDocument.openKey(root, layout.profileConfig(profile));
|
|
234
235
|
if (dropGrantRow(document, key)) await document.save();
|
|
235
236
|
}
|
|
236
237
|
|
|
@@ -37,9 +37,9 @@ export interface IdentityListing {
|
|
|
37
37
|
readonly reachable: boolean;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** Whether a rule list puts `
|
|
40
|
+
/** Whether a rule list puts `lanes_identity.*` in force, in either spelling. */
|
|
41
41
|
function covers(rules: ReadonlyArray<{ capability: string }>): boolean {
|
|
42
|
-
return rules.some((rule) => rule.capability === '*' || rule.capability === '
|
|
42
|
+
return rules.some((rule) => rule.capability === '*' || rule.capability === 'lanes_identity.*');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -61,7 +61,7 @@ function readable(config: {
|
|
|
61
61
|
// (ADR-058). The old shape could say "declared but not granted" and
|
|
62
62
|
// "granted but not declared", and both served nothing while reading like
|
|
63
63
|
// configuration that worked; neither is expressible here.
|
|
64
|
-
const grant = config.grants.find((row) => row.connection.startsWith('
|
|
64
|
+
const grant = config.grants.find((row) => row.connection.startsWith('lanes_identity.'));
|
|
65
65
|
if (grant === undefined) return false;
|
|
66
66
|
|
|
67
67
|
return covers(grant.allow) && !covers(grant.deny);
|
|
@@ -244,7 +244,7 @@ export async function identityList(options: { json?: boolean } & GlobalFlags): P
|
|
|
244
244
|
print(warn('declared, but no agent can read it'));
|
|
245
245
|
print(
|
|
246
246
|
style.dim(
|
|
247
|
-
' the identity surface needs a connection row and
|
|
247
|
+
' the identity surface needs a connection row and a lanes_identity.* allow rule;',
|
|
248
248
|
),
|
|
249
249
|
);
|
|
250
250
|
print(style.dim(' adding an entry with this command writes both.'));
|
|
@@ -115,11 +115,7 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
|
|
|
115
115
|
|
|
116
116
|
// Nothing is written until this returns: the token is asked for, the
|
|
117
117
|
// repository is probed, and either can refuse while the profile is intact.
|
|
118
|
-
const secrets = await openSecretStoreFor(
|
|
119
|
-
runtime.config,
|
|
120
|
-
runtime.resolution.workspaceRoot,
|
|
121
|
-
runtime.target,
|
|
122
|
-
);
|
|
118
|
+
const secrets = await openSecretStoreFor(runtime.resolution.workspaceRoot, runtime.target);
|
|
123
119
|
const token = await resolveToken(secrets, knowledge.token_ref, knowledge.repo, selection, {
|
|
124
120
|
replace: flags.replace,
|
|
125
121
|
});
|
|
@@ -205,11 +201,7 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
|
|
|
205
201
|
|
|
206
202
|
// Built here rather than taken from `runtime.knowledge`, so both directions
|
|
207
203
|
// of this command reach the repository through one constructor.
|
|
208
|
-
const secrets = await openSecretStoreFor(
|
|
209
|
-
runtime.config,
|
|
210
|
-
runtime.resolution.workspaceRoot,
|
|
211
|
-
runtime.target,
|
|
212
|
-
);
|
|
204
|
+
const secrets = await openSecretStoreFor(runtime.resolution.workspaceRoot, runtime.target);
|
|
213
205
|
const token = await resolveToken(secrets, knowledge.token_ref, knowledge.repo, selection);
|
|
214
206
|
const repository = repositoryFor(knowledge, token, flags.fetch);
|
|
215
207
|
|
|
@@ -270,12 +262,13 @@ async function openLocalStores(
|
|
|
270
262
|
},
|
|
271
263
|
runtime.credentials,
|
|
272
264
|
);
|
|
273
|
-
// The *granted* connection,
|
|
274
|
-
// meaning without changing arity at ADR-059, so the compiler was silent
|
|
275
|
-
|
|
265
|
+
// The *granted* connection, beside the profile. `layout.skills` changed
|
|
266
|
+
// meaning without changing arity at ADR-059, so the compiler was silent;
|
|
267
|
+
// ADR-066 put the profile back in front of it, and it is an argument now.
|
|
268
|
+
const skillsConnection = soleGrantFor(runtime.config, 'lanes_skills');
|
|
276
269
|
return {
|
|
277
270
|
storage: factory(),
|
|
278
|
-
skills: skillsConnection === undefined ? null : factory(layout.skills(skillsConnection)),
|
|
271
|
+
skills: skillsConnection === undefined ? null : factory(layout.skills(runtime.config.instance.profile, skillsConnection)),
|
|
279
272
|
};
|
|
280
273
|
}
|
|
281
274
|
|
|
@@ -349,7 +342,7 @@ function grantedInstances(config: Parameters<typeof soleGrantFor>[0]): {
|
|
|
349
342
|
entities: string;
|
|
350
343
|
} {
|
|
351
344
|
return {
|
|
352
|
-
memory: soleGrantFor(config, '
|
|
353
|
-
entities: soleGrantFor(config, '
|
|
345
|
+
memory: soleGrantFor(config, 'lanes_memory') ?? 'main',
|
|
346
|
+
entities: soleGrantFor(config, 'lanes_entities') ?? 'main',
|
|
354
347
|
};
|
|
355
348
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ConfigError,
|
|
3
3
|
KNOWLEDGE_LAYOUT,
|
|
4
|
+
KNOWLEDGE_PREFIX,
|
|
4
5
|
knowledgeRoot,
|
|
5
6
|
type KnowledgeArea,
|
|
6
7
|
type KnowledgeConfig,
|
|
@@ -81,10 +82,14 @@ function localAreas(
|
|
|
81
82
|
// personal's repository and then deleted them locally, leaving `work`
|
|
82
83
|
// reading an empty store with no knowledge block of its own. `skills` was
|
|
83
84
|
// already connection-scoped, which is why it alone was correct.
|
|
85
|
+
// `scope` reads the *local* store and `prefix` names the repository
|
|
86
|
+
// directory, and since contract 4 those are different strings:
|
|
87
|
+
// `lanes_memory/lan1/` on disk becomes `memory/lan1/` in the repository.
|
|
88
|
+
// Reading one where the other belongs finds nothing and reports success.
|
|
84
89
|
memory: {
|
|
85
90
|
store: storage,
|
|
86
|
-
scope: `${
|
|
87
|
-
prefix: `${
|
|
91
|
+
scope: `${KNOWLEDGE_PREFIX.memory}/${instances.memory}/`,
|
|
92
|
+
prefix: `${KNOWLEDGE_PREFIX.memory}/`,
|
|
88
93
|
},
|
|
89
94
|
// Null becomes an empty area rather than a refusal: a profile granting no
|
|
90
95
|
// skills connection has none to move, and `knowledge use` should still move
|
|
@@ -93,8 +98,8 @@ function localAreas(
|
|
|
93
98
|
skills: { store: skills ?? EMPTY_AREA, scope: '', prefix: '' },
|
|
94
99
|
entities: {
|
|
95
100
|
store: storage,
|
|
96
|
-
scope: `${
|
|
97
|
-
prefix: `${
|
|
101
|
+
scope: `${KNOWLEDGE_PREFIX.entities}/${instances.entities}/`,
|
|
102
|
+
prefix: `${KNOWLEDGE_PREFIX.entities}/`,
|
|
98
103
|
},
|
|
99
104
|
};
|
|
100
105
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KNOWLEDGE_LAYOUT, layout, soleGrantFor } from '#profile';
|
|
1
|
+
import { KNOWLEDGE_LAYOUT, KNOWLEDGE_PREFIX, layout, soleGrantFor } from '#profile';
|
|
2
2
|
import { announce, emit, heading, print, style, table } from '../../output.ts';
|
|
3
3
|
import { openRuntime } from '../../runtime.ts';
|
|
4
4
|
import type { KnowledgeFlags } from './index.ts';
|
|
@@ -20,12 +20,16 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
|
|
|
20
20
|
const profile = runtime.config.instance.profile;
|
|
21
21
|
const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
|
|
22
22
|
const skills = runtime.skills ? (await runtime.skills.list()).length : 0;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Counted through the *provider's* prefix, which is what the local store is
|
|
24
|
+
// scoped into; `KNOWLEDGE_LAYOUT` is the repository's directory name and
|
|
25
|
+
// since contract 4 the two are different strings. Counting by the wrong one
|
|
26
|
+
// reports zero for a profile whose memory is perfectly well populated.
|
|
27
|
+
const memory = (await runtime.storage.list(`${KNOWLEDGE_PREFIX.memory}/`)).length;
|
|
28
|
+
const entities = (await runtime.storage.list(`${KNOWLEDGE_PREFIX.entities}/`)).length;
|
|
25
29
|
const where = runtime.knowledge?.describe;
|
|
26
30
|
// Null when no skills connection is granted (ADR-059). The row still prints,
|
|
27
31
|
// saying so: a missing row reads as "there are none", not "not granted".
|
|
28
|
-
const skillsConnection = soleGrantFor(runtime.config, '
|
|
32
|
+
const skillsConnection = soleGrantFor(runtime.config, 'lanes_skills');
|
|
29
33
|
|
|
30
34
|
if (flags.json) {
|
|
31
35
|
print(
|
|
@@ -41,11 +45,12 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
|
|
|
41
45
|
heading('Knowledge');
|
|
42
46
|
table([
|
|
43
47
|
// The memory *directory*, not the blob root it sits in. `layout.blobs`
|
|
44
|
-
// is
|
|
45
|
-
// printing it here would name a directory that is
|
|
48
|
+
// is the profile's own directory, which is where every provider's
|
|
49
|
+
// namespace lives — printing it here would name a directory that is
|
|
50
|
+
// mostly not memory.
|
|
46
51
|
[
|
|
47
52
|
' memory',
|
|
48
|
-
where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs()}/${
|
|
53
|
+
where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs(profile)}/${KNOWLEDGE_PREFIX.memory}`,
|
|
49
54
|
style.dim(`${memory} file${memory === 1 ? '' : 's'}`),
|
|
50
55
|
],
|
|
51
56
|
[
|
|
@@ -53,7 +58,7 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
|
|
|
53
58
|
where
|
|
54
59
|
? `${where}/${KNOWLEDGE_LAYOUT.skills}`
|
|
55
60
|
: skillsConnection
|
|
56
|
-
? layout.skills(skillsConnection)
|
|
61
|
+
? layout.skills(profile, skillsConnection)
|
|
57
62
|
: style.dim('not granted'),
|
|
58
63
|
style.dim(`${skills} file${skills === 1 ? '' : 's'}`),
|
|
59
64
|
],
|
|
@@ -64,7 +69,7 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
|
|
|
64
69
|
' entities',
|
|
65
70
|
where
|
|
66
71
|
? `${where}/${KNOWLEDGE_LAYOUT.entities}`
|
|
67
|
-
: `${layout.blobs()}/${
|
|
72
|
+
: `${layout.blobs(profile)}/${KNOWLEDGE_PREFIX.entities}`,
|
|
68
73
|
style.dim(`${entities} file${entities === 1 ? '' : 's'}`),
|
|
69
74
|
],
|
|
70
75
|
]);
|
|
@@ -71,7 +71,7 @@ export async function migratedRenamedProviders(
|
|
|
71
71
|
// Throws when it is malformed beyond a rename, which is a better sentence
|
|
72
72
|
// than the referential one it would otherwise be reported under.
|
|
73
73
|
const config = shapeOf(await ConfigDocument.open(root, selection.profile));
|
|
74
|
-
const credentials = await openSecretStoreFor(
|
|
74
|
+
const credentials = await openSecretStoreFor(root, target);
|
|
75
75
|
|
|
76
76
|
const migration = await migrateRenamedProviders(document, profiles, credentials, {
|
|
77
77
|
apply: flags.fix === true,
|
|
@@ -158,7 +158,10 @@ export async function migratedContract(flags: RenameFlags, refusal: unknown): Pr
|
|
|
158
158
|
// only about contract 1 — so a contract-2 bucket, the exact thing this
|
|
159
159
|
// function exists to rescue, returned false here and fell through to the
|
|
160
160
|
// refusal it was supposed to fix. With `--fix` absent this writes nothing.
|
|
161
|
-
const migration = await migrateToCurrentContract(where, {
|
|
161
|
+
const migration = await migrateToCurrentContract(where, {
|
|
162
|
+
apply: flags.fix === true,
|
|
163
|
+
target,
|
|
164
|
+
});
|
|
162
165
|
if (migration.alreadyCurrent) return false;
|
|
163
166
|
|
|
164
167
|
const applied = flags.fix === true;
|
|
@@ -121,7 +121,7 @@ export async function pair(flags: PairFlags, deps: PairDeps = {}): Promise<void>
|
|
|
121
121
|
|
|
122
122
|
const chosen = named ?? profiles[0]!;
|
|
123
123
|
const host = chosen.config.instance.host;
|
|
124
|
-
const credentials = await openSecretStoreFor(
|
|
124
|
+
const credentials = await openSecretStoreFor(root, target);
|
|
125
125
|
|
|
126
126
|
// A workspace that declares a deployment is paired over the address the
|
|
127
127
|
// platform gave it, not over loopback — which is what `declared.deploy`
|
|
@@ -2,7 +2,7 @@ import { readSession } from '#auth/lanes/session.ts';
|
|
|
2
2
|
import { ConfigError } from '#profile';
|
|
3
3
|
import { startEndpoint } from '#server/endpoint.ts';
|
|
4
4
|
import { streamLogger } from '#server/logging.ts';
|
|
5
|
-
import { repairOwnerLayer } from '../../config-repair.ts';
|
|
5
|
+
import { repairOwnerLayer } from '../../config-repair-sweep.ts';
|
|
6
6
|
import { announce, ok, print, style, warn } from '../../output.ts';
|
|
7
7
|
import { staleNudge } from '../../release.ts';
|
|
8
8
|
import { primaryProfile, resolveProfile, type GlobalFlags } from '../../runtime.ts';
|
|
@@ -127,6 +127,6 @@ export async function assetsRemove(name: string | undefined, flags: OwnerFlags):
|
|
|
127
127
|
* it is built from the same two functions rather than spelled as a path.
|
|
128
128
|
*/
|
|
129
129
|
export function assetsStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
|
|
130
|
-
const connection = ownerConnection(runtime.config, '
|
|
131
|
-
return scopeBlobStore(runtime.storage, scopeNamespace('
|
|
130
|
+
const connection = ownerConnection(runtime.config, 'lanes_assets', flags);
|
|
131
|
+
return scopeBlobStore(runtime.storage, scopeNamespace('lanes_assets', connection));
|
|
132
132
|
}
|
|
@@ -239,6 +239,6 @@ export async function entitiesReindex(flags: OwnerFlags): Promise<void> {
|
|
|
239
239
|
* `memoryStore` gives: a path spelled out again is a path that can differ.
|
|
240
240
|
*/
|
|
241
241
|
export function entitiesStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
|
|
242
|
-
const connection = ownerConnection(runtime.config, '
|
|
243
|
-
return scopeBlobStore(runtime.storage, scopeNamespace('
|
|
242
|
+
const connection = ownerConnection(runtime.config, 'lanes_entities', flags);
|
|
243
|
+
return scopeBlobStore(runtime.storage, scopeNamespace('lanes_entities', connection));
|
|
244
244
|
}
|