@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { ConfigError, describeRename } from './load.ts';
|
|
2
|
+
import type { Config, ConnectionConfig, GrantConfig, TargetConfig } from './schema.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The join between a profile and the workspace it lives in.
|
|
6
|
+
*
|
|
7
|
+
* A connection is declared once, in `connections.yaml`, and a profile names the
|
|
8
|
+
* ones it selects in `grants:` (ADR-057). Nothing downstream should perform
|
|
9
|
+
* that join itself: `dispatch`, `visibility`, `status`, `setup` and the
|
|
10
|
+
* reconciler all want the same answer — *which accounts does this profile
|
|
11
|
+
* reach, and what may be done with each* — and three implementations of one
|
|
12
|
+
* join is three chances for discovery and enforcement to disagree, which is the
|
|
13
|
+
* failure `allowedConnections` exists to prevent on the capability axis.
|
|
14
|
+
*
|
|
15
|
+
* So this file answers it once and everything else asks.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** `<provider>.<id>` — the same string an agent passes as `connection`. */
|
|
19
|
+
export function connectionRefOf(connection: ConnectionConfig): string {
|
|
20
|
+
return `${connection.provider}.${connection.id}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One account this profile reaches, with the rules that govern it.
|
|
25
|
+
*
|
|
26
|
+
* The grant travels with the connection deliberately. Every caller that has a
|
|
27
|
+
* connection in its hand is about to ask what may be done with it, and handing
|
|
28
|
+
* back a pair removes the second lookup — along with the possibility of pairing
|
|
29
|
+
* a connection with the wrong profile's grant, which is the one mistake this
|
|
30
|
+
* shape makes unrepresentable.
|
|
31
|
+
*/
|
|
32
|
+
export interface SelectedConnection {
|
|
33
|
+
readonly ref: string;
|
|
34
|
+
readonly connection: ConnectionConfig;
|
|
35
|
+
readonly grant: GrantConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The connections a profile selects, in the order it declares them.
|
|
40
|
+
*
|
|
41
|
+
* Declaration order rather than the workspace's, because the profile is what
|
|
42
|
+
* this is a view of, and a listing that reordered the owner's own file would be
|
|
43
|
+
* answering a question nobody asked. A grant naming a connection that does not
|
|
44
|
+
* exist is skipped here and refused by `assertGrantsResolve` at load — this
|
|
45
|
+
* function is a view, not a gate, and a view that threw would make every
|
|
46
|
+
* listing depend on the config being valid.
|
|
47
|
+
*/
|
|
48
|
+
export function selectConnections(
|
|
49
|
+
config: Config,
|
|
50
|
+
available: readonly ConnectionConfig[],
|
|
51
|
+
): SelectedConnection[] {
|
|
52
|
+
const byRef = new Map(available.map((connection) => [connectionRefOf(connection), connection]));
|
|
53
|
+
|
|
54
|
+
return config.grants.flatMap((grant) => {
|
|
55
|
+
const connection = byRef.get(grant.connection);
|
|
56
|
+
return connection === undefined ? [] : [{ ref: grant.connection, connection, grant }];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every reference in a profile resolves, and nothing is granted twice.
|
|
62
|
+
*
|
|
63
|
+
* Two failures, and the second is the one worth having a check for. A grant
|
|
64
|
+
* naming a connection the workspace does not hold is visible the moment anyone
|
|
65
|
+
* looks — the surface is simply absent. Two grants naming the *same* connection
|
|
66
|
+
* is not: one of them silently wins, and which one depends on iteration order,
|
|
67
|
+
* so a profile could deny a capability in one row and allow it in another and
|
|
68
|
+
* behave differently between releases. Refusing at load is what keeps
|
|
69
|
+
* `grants:` a set rather than a sequence with precedence.
|
|
70
|
+
*/
|
|
71
|
+
export function assertGrantsResolve(
|
|
72
|
+
config: Config,
|
|
73
|
+
available: readonly ConnectionConfig[],
|
|
74
|
+
): void {
|
|
75
|
+
const refs = new Set(available.map(connectionRefOf));
|
|
76
|
+
|
|
77
|
+
const missing = config.grants
|
|
78
|
+
.map((grant) => grant.connection)
|
|
79
|
+
.filter((ref) => !refs.has(ref));
|
|
80
|
+
|
|
81
|
+
if (missing.length > 0) {
|
|
82
|
+
throw new ConfigError(
|
|
83
|
+
`Profile "${config.instance.profile}" grants a connection this workspace does not hold: ` +
|
|
84
|
+
`${[...new Set(missing)].join(', ')}.\n` +
|
|
85
|
+
` Connections live in connections.yaml. Run "lanes link connection list" to see them.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const seen = new Set<string>();
|
|
90
|
+
const duplicated = config.grants
|
|
91
|
+
.map((grant) => grant.connection)
|
|
92
|
+
.filter((ref) => (seen.has(ref) ? true : (seen.add(ref), false)));
|
|
93
|
+
|
|
94
|
+
if (duplicated.length > 0) {
|
|
95
|
+
throw new ConfigError(
|
|
96
|
+
`Profile "${config.instance.profile}" grants the same connection more than once: ` +
|
|
97
|
+
`${[...new Set(duplicated)].join(', ')}.\n` +
|
|
98
|
+
` One row per connection — merge the allow and deny lists into it.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* No two connections in a workspace share a reference.
|
|
105
|
+
*
|
|
106
|
+
* `<provider>.<id>` is what a credential ref is derived from and what an agent
|
|
107
|
+
* names, so a duplicate is two accounts answering to one address. It was
|
|
108
|
+
* impossible to express before contract 3 only because a profile held its own
|
|
109
|
+
* list; one workspace-wide list makes it expressible, so it has to be refused.
|
|
110
|
+
*/
|
|
111
|
+
export function assertConnectionsUnique(connections: readonly ConnectionConfig[]): void {
|
|
112
|
+
const seen = new Set<string>();
|
|
113
|
+
const duplicated = connections
|
|
114
|
+
.map(connectionRefOf)
|
|
115
|
+
.filter((ref) => (seen.has(ref) ? true : (seen.add(ref), false)));
|
|
116
|
+
|
|
117
|
+
if (duplicated.length > 0) {
|
|
118
|
+
throw new ConfigError(
|
|
119
|
+
`connections.yaml declares the same connection more than once: ` +
|
|
120
|
+
`${[...new Set(duplicated)].join(', ')}.\n` +
|
|
121
|
+
` A connection is addressed as "<provider>.<id>", so two rows sharing one ` +
|
|
122
|
+
`are two accounts at one address.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The one connection this profile grants for a single-instance provider.
|
|
129
|
+
*
|
|
130
|
+
* `undefined` when it grants none, which is a legitimate state: a profile that
|
|
131
|
+
* denies `skills.*` reaches no skills, and the surface is absent rather than
|
|
132
|
+
* empty.
|
|
133
|
+
*/
|
|
134
|
+
export function soleGrantFor(config: Config, provider: string): string | undefined {
|
|
135
|
+
const prefix = `${provider}.`;
|
|
136
|
+
const granted = config.grants.find((grant) => grant.connection.startsWith(prefix));
|
|
137
|
+
return granted?.connection.slice(prefix.length);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Where this profile's vault document lives, in one spelling.
|
|
142
|
+
*
|
|
143
|
+
* **Two places derived this, and they disagreed.** `openVault` names it per
|
|
144
|
+
* connection (ADR-059) — `vault/main` for a profile granting `vault.main` — and
|
|
145
|
+
* the deploy's provisioning step named `vault/document`, the contract-2 constant.
|
|
146
|
+
* So every deployed workspace that did not hand-write `vault.ref` created and
|
|
147
|
+
* granted one secret and then asked Secret Manager for another: the revision got
|
|
148
|
+
* `PERMISSION_DENIED` on the ref nothing had made, exited 1, and never listened
|
|
149
|
+
* on its port. A deploy that hand-wrote a `ref` worked, which is what kept this
|
|
150
|
+
* out of sight — a rehearsal that sets one is testing the path nobody takes.
|
|
151
|
+
*
|
|
152
|
+
* `ref` still wins where it is written, because a deployment already sealing
|
|
153
|
+
* under one name has to keep opening it.
|
|
154
|
+
*/
|
|
155
|
+
export function vaultRef(declared: TargetConfig | undefined, config: Config): string {
|
|
156
|
+
// **The profile, then the connection.** This was `vault/<connection>`, which
|
|
157
|
+
// was distinct per profile only while each profile had its own vault
|
|
158
|
+
// instance. ADR-066 merges the owner layer to one row per surface, so every
|
|
159
|
+
// profile grants `lanes_vault.lan5` and every profile opened one sealed
|
|
160
|
+
// document — `vault_put` from `personal` overwriting `work`'s item of the
|
|
161
|
+
// same id, and `vault_get` reading the other profile's credential. The `file`
|
|
162
|
+
// and `blob` adapters take the profile from `layout`; this is the same fact
|
|
163
|
+
// for the adapter every deployment uses.
|
|
164
|
+
//
|
|
165
|
+
// A `ref` the target states outright still wins: a deployment already sealing
|
|
166
|
+
// under one name has to keep opening it.
|
|
167
|
+
const connection = soleGrantFor(config, 'lanes_vault') ?? 'main';
|
|
168
|
+
return declared?.vault?.ref ?? `vault/${config.instance.profile}/${connection}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A connection whose provider id moved out from under it.
|
|
173
|
+
*
|
|
174
|
+
* `tasks` is the built-in task list; Google's is `google_tasks` (ADR-051). A row
|
|
175
|
+
* that still says `tasks` and names an address rather than the built-in's own
|
|
176
|
+
* label is somebody's Google account resolving to the wrong provider — and the
|
|
177
|
+
* failure is silent in the worst way: the row loads, reconcile calls it active,
|
|
178
|
+
* and every call goes to an empty local store.
|
|
179
|
+
*
|
|
180
|
+
* Here rather than in the profile loader, because the evidence is the *account*
|
|
181
|
+
* and accounts live in this file.
|
|
182
|
+
*/
|
|
183
|
+
export function assertNoRenamedProviders(
|
|
184
|
+
connections: readonly ConnectionConfig[],
|
|
185
|
+
repair: string,
|
|
186
|
+
): void {
|
|
187
|
+
const problems = connections.flatMap((connection, index) => {
|
|
188
|
+
const renamed = describeRename(connection, repair);
|
|
189
|
+
return renamed === null ? [] : [`connections[${index}]: ${renamed}`];
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (problems.length > 0) {
|
|
193
|
+
throw new ConfigError(`connections.yaml:\n${problems.map((p) => ` ${p}`).join('\n')}`, problems);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { parseDocument } from 'yaml';
|
|
2
2
|
import { readWorkspaceFile, workspaceFiles, writeWorkspaceFile } from './files.ts';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SUPPORTED_CONTRACT,
|
|
5
|
+
workspaceSchema,
|
|
6
|
+
workspaceTargetSchema,
|
|
7
|
+
type WorkspaceTarget,
|
|
8
|
+
} from './schema.ts';
|
|
4
9
|
import { WORKSPACE_FILE } from './workspace.ts';
|
|
10
|
+
import { LEGACY_WORKSPACE_FILE } from './layout.ts';
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* Writing the target registry.
|
|
@@ -74,24 +80,96 @@ async function editRegistry(
|
|
|
74
80
|
edit: (targets: Record<string, WorkspaceTarget>) => void,
|
|
75
81
|
): Promise<void> {
|
|
76
82
|
const files = workspaceFiles(workspaceRoot);
|
|
83
|
+
|
|
84
|
+
// **Either name, and the one it read is the one it writes.** Reading only the
|
|
85
|
+
// new name meant an unmigrated workspace fell through to a bare
|
|
86
|
+
// `contract: 4` document: `deploy` then wrote a registry holding one target
|
|
87
|
+
// beside the `lanes-link.yaml` that declared them all, and `readWorkspace`
|
|
88
|
+
// prefers the new name — so every other target vanished from a command whose
|
|
89
|
+
// job was to record one.
|
|
90
|
+
const held = await readWorkspaceFile(files, WORKSPACE_FILE);
|
|
91
|
+
const key = held === null ? LEGACY_WORKSPACE_FILE : WORKSPACE_FILE;
|
|
77
92
|
const text =
|
|
78
|
-
|
|
93
|
+
held ??
|
|
94
|
+
(await readWorkspaceFile(files, LEGACY_WORKSPACE_FILE)) ??
|
|
95
|
+
`contract: ${SUPPORTED_CONTRACT}\n`;
|
|
79
96
|
|
|
80
97
|
const document = parseDocument(text);
|
|
81
|
-
const
|
|
98
|
+
const raw = (document.toJSON() ?? {}) as {
|
|
99
|
+
workspaces?: Record<string, WorkspaceTarget>;
|
|
100
|
+
targets?: Record<string, LegacyTargetEntry>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// **Whichever block this file already keeps its registry in.**
|
|
104
|
+
//
|
|
105
|
+
// This read and wrote `workspaces:` unconditionally. A contract-2 file keeps
|
|
106
|
+
// it under `targets:`, so recording a deploy into one found no registry,
|
|
107
|
+
// added the entry to an empty object, and wrote a *second* block beside the
|
|
108
|
+
// first. `workspaceSchema` has no `targets` key and zod strips what it does
|
|
109
|
+
// not declare, so the hybrid validated and landed.
|
|
110
|
+
//
|
|
111
|
+
// That is reachable from an ordinary command: `deploy` migrates the target
|
|
112
|
+
// workspace, never the local one, so deploying to a bucket from a laptop that
|
|
113
|
+
// has not run `update` yet does exactly this. The result is a file whose two
|
|
114
|
+
// registries disagree — and `rewriteRegistry` then rebuilds `workspaces:`
|
|
115
|
+
// from the stale `targets:`, discarding the newer record without a word.
|
|
116
|
+
//
|
|
117
|
+
// Writing into the block the file already has keeps it coherent at whatever
|
|
118
|
+
// contract it is, and leaves converting the two to the migration that owns
|
|
119
|
+
// that job.
|
|
120
|
+
const legacy = raw.workspaces === undefined && raw.targets !== undefined;
|
|
121
|
+
const block = legacy ? 'targets' : 'workspaces';
|
|
122
|
+
const targets = legacy ? current(raw.targets ?? {}) : (raw.workspaces ?? {});
|
|
82
123
|
|
|
83
124
|
edit(targets);
|
|
84
125
|
|
|
85
|
-
if (Object.keys(targets).length === 0) document.deleteIn([
|
|
86
|
-
else document.setIn([
|
|
126
|
+
if (Object.keys(targets).length === 0) document.deleteIn([block]);
|
|
127
|
+
else document.setIn([block], sorted(legacy ? asLegacy(targets) : targets));
|
|
87
128
|
|
|
88
129
|
// Validated before it lands, on the rendered tree rather than the input, so
|
|
89
130
|
// what is checked is what would be read back.
|
|
90
|
-
|
|
131
|
+
//
|
|
132
|
+
// `workspaceSchema` declares no `targets` key and zod strips what it does not
|
|
133
|
+
// declare, so on the legacy branch parsing the whole document checks nothing
|
|
134
|
+
// at all — an entry that is neither pointer nor declaration, or one trying to
|
|
135
|
+
// be both, landed silently and only surfaced weeks later when the migration
|
|
136
|
+
// converted it and refused. The entries are checked directly there, in the
|
|
137
|
+
// contract-3 shape `current` normalised them to.
|
|
138
|
+
if (legacy) for (const entry of Object.values(targets)) workspaceTargetSchema.parse(entry);
|
|
139
|
+
else workspaceSchema.parse(document.toJSON());
|
|
91
140
|
|
|
92
|
-
await writeWorkspaceFile(files,
|
|
141
|
+
await writeWorkspaceFile(files, key, String(document));
|
|
93
142
|
}
|
|
94
143
|
|
|
95
|
-
function sorted(targets: Record<string,
|
|
144
|
+
function sorted<T>(targets: Record<string, T>): Record<string, T> {
|
|
96
145
|
return Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b)));
|
|
97
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A contract-2 entry spelled a pointer `workspace:`; contract 3 spells it `at:`.
|
|
150
|
+
*
|
|
151
|
+
* Normalised on the way in and back on the way out, so `edit` and `pick` see one
|
|
152
|
+
* shape and never have to ask which contract they are looking at — and so a
|
|
153
|
+
* legacy file keeps the spelling its own schema expects.
|
|
154
|
+
*/
|
|
155
|
+
interface LegacyTargetEntry extends Omit<WorkspaceTarget, 'at'> {
|
|
156
|
+
workspace?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function current(targets: Record<string, LegacyTargetEntry>): Record<string, WorkspaceTarget> {
|
|
160
|
+
return Object.fromEntries(
|
|
161
|
+
Object.entries(targets).map(([name, entry]) => {
|
|
162
|
+
const { workspace, ...rest } = entry;
|
|
163
|
+
return [name, workspace === undefined ? rest : { at: workspace, ...rest }];
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function asLegacy(targets: Record<string, WorkspaceTarget>): Record<string, LegacyTargetEntry> {
|
|
169
|
+
return Object.fromEntries(
|
|
170
|
+
Object.entries(targets).map(([name, entry]) => {
|
|
171
|
+
const { at, ...rest } = entry;
|
|
172
|
+
return [name, at === undefined ? rest : { workspace: at, ...rest }];
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
package/src/profile/index.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A profile: its file, its schema, and where it lives on disk.
|
|
3
3
|
*
|
|
4
|
-
* **
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* **A profile is a selection, not an inventory.** Since contract 3 the accounts
|
|
5
|
+
* live in the workspace's `connections.yaml` and a profile names the ones it
|
|
6
|
+
* grants (ADR-057), so profiles share an endpoint, its token, the credential
|
|
7
|
+
* store, and any connection they both select. What they do not share is the
|
|
8
|
+
* grant: two profiles on one mailbox can permit entirely different things
|
|
9
|
+
* (ADR-058). This component owns everything about what a profile *is* — the
|
|
10
|
+
* YAML contract, the loader that validates it, the join onto the workspace's
|
|
11
|
+
* connections, and the resolution that finds them.
|
|
8
12
|
*
|
|
9
13
|
* What it deliberately does not own: which providers exist (`#registry`) and
|
|
10
14
|
* how a call runs (`#dispatch`). Those were all one package called `core`,
|
|
@@ -15,23 +19,44 @@ export {
|
|
|
15
19
|
DEPLOY_DEFAULTS,
|
|
16
20
|
SUPPORTED_CONTRACT,
|
|
17
21
|
configSchema,
|
|
22
|
+
SINGLE_INSTANCE_PROVIDERS,
|
|
23
|
+
connectionsFileSchema,
|
|
18
24
|
declaredTarget,
|
|
25
|
+
grantSchema,
|
|
19
26
|
isPointer,
|
|
27
|
+
memberSchema,
|
|
20
28
|
workspaceSchema,
|
|
21
29
|
workspaceTargetSchema,
|
|
22
30
|
type AuthorizationConfig,
|
|
23
31
|
type Config,
|
|
24
32
|
type ConnectionConfig,
|
|
33
|
+
type ConnectionsFile,
|
|
25
34
|
type DeployConfig,
|
|
35
|
+
type GrantConfig,
|
|
26
36
|
type IdentityEntry,
|
|
37
|
+
type MemberConfig,
|
|
27
38
|
type PolicyRuleConfig,
|
|
28
39
|
type TargetConfig,
|
|
29
40
|
type WorkspaceConfig,
|
|
30
41
|
type WorkspaceTarget,
|
|
31
42
|
} from './schema.ts';
|
|
32
43
|
|
|
44
|
+
export {
|
|
45
|
+
assertConnectionsUnique,
|
|
46
|
+
assertGrantsResolve,
|
|
47
|
+
assertNoRenamedProviders,
|
|
48
|
+
connectionRefOf,
|
|
49
|
+
selectConnections,
|
|
50
|
+
soleGrantFor,
|
|
51
|
+
vaultRef,
|
|
52
|
+
type SelectedConnection,
|
|
53
|
+
} from './connections.ts';
|
|
54
|
+
|
|
55
|
+
export { PAIR_CERT_REF, PAIR_KEY_REF, PAIR_TOKEN_REF } from './pairing.ts';
|
|
56
|
+
|
|
33
57
|
export {
|
|
34
58
|
KNOWLEDGE_LAYOUT,
|
|
59
|
+
KNOWLEDGE_PREFIX,
|
|
35
60
|
knowledgeRoot,
|
|
36
61
|
knowledgeTargetSchema,
|
|
37
62
|
parseRepository,
|
|
@@ -103,13 +128,17 @@ export {
|
|
|
103
128
|
writeWorkspaceFile,
|
|
104
129
|
} from './files.ts';
|
|
105
130
|
export {
|
|
131
|
+
CONNECTIONS_FILE,
|
|
132
|
+
readConnections,
|
|
106
133
|
type ProfileSelection,
|
|
107
134
|
type Resolution,
|
|
108
135
|
type ResolveOptions,
|
|
109
136
|
} from './workspace.ts';
|
|
110
137
|
|
|
111
138
|
export {
|
|
112
|
-
|
|
139
|
+
PROFILE_FILE,
|
|
140
|
+
LEGACY_DATA_DIR,
|
|
141
|
+
LEGACY_WORKSPACE_FILE,
|
|
142
|
+
legacyProfileConfig,
|
|
113
143
|
layout,
|
|
114
|
-
profileDir,
|
|
115
144
|
} from './layout.ts';
|
package/src/profile/knowledge.ts
CHANGED
|
@@ -96,11 +96,17 @@ export type KnowledgeConfig = z.infer<typeof knowledgeTargetSchema>;
|
|
|
96
96
|
/**
|
|
97
97
|
* Where each area sits inside the repository.
|
|
98
98
|
*
|
|
99
|
-
* Three directories, named after the three things that move
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
99
|
+
* Three directories, named after the three things that move — and named for a
|
|
100
|
+
* *reader*, because a knowledge repository is somebody's own and they browse it
|
|
101
|
+
* on GitHub. `memory/` is what belongs at the top of that tree, not
|
|
102
|
+
* `lanes_memory/`.
|
|
103
|
+
*
|
|
104
|
+
* **This used to be the provider's blob prefix as well**, on the reasoning that
|
|
105
|
+
* one word doing both jobs could not come to disagree with itself. Contract 4
|
|
106
|
+
* prefixed the owner layer (`lanes_memory`), so the two are no longer the same
|
|
107
|
+
* string and the choice has to be made rather than avoided: the route below
|
|
108
|
+
* reads `PROVIDER_PREFIX`, and this stays the readable name. `KNOWLEDGE_ROUTES`
|
|
109
|
+
* pairs them in one place so a rename still cannot move one without the other.
|
|
104
110
|
*
|
|
105
111
|
* Declared here, beside the schema, so the runtime that opens these stores and
|
|
106
112
|
* the command that migrates into them read one definition.
|
|
@@ -113,6 +119,13 @@ export const KNOWLEDGE_LAYOUT = {
|
|
|
113
119
|
|
|
114
120
|
export type KnowledgeArea = keyof typeof KNOWLEDGE_LAYOUT;
|
|
115
121
|
|
|
122
|
+
/** The blob prefix each area is scoped into locally — the provider's own id. */
|
|
123
|
+
export const KNOWLEDGE_PREFIX = {
|
|
124
|
+
memory: 'lanes_memory',
|
|
125
|
+
skills: 'lanes_skills',
|
|
126
|
+
entities: 'lanes_entities',
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
116
129
|
/** The directory one area occupies, under the profile's optional path prefix. */
|
|
117
130
|
export function knowledgeRoot(knowledge: KnowledgeConfig, area: KnowledgeArea): string {
|
|
118
131
|
const directory = KNOWLEDGE_LAYOUT[area];
|