@lanes-sh/link 0.7.2 → 0.8.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 +136 -61
- 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 +65 -0
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +108 -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 +88 -87
- 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 +4 -2
- 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 +182 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +21 -9
- package/src/cli/commands/knowledge/index.ts +46 -79
- package/src/cli/commands/knowledge/migrate.ts +74 -13
- package/src/cli/commands/knowledge/show.ts +92 -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 +29 -12
- 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 +52 -4
- 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/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/profile/removal.ts +79 -76
- package/src/cli/commands/profile/remove.ts +16 -1
- package/src/cli/commands/profile.ts +46 -10
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +34 -12
- 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.ts +58 -17
- package/src/cli/config-edit.ts +75 -140
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair.ts +89 -32
- package/src/cli/config-templates.ts +198 -0
- package/src/cli/contract3-data.ts +328 -0
- package/src/cli/contract3-shape.ts +186 -0
- package/src/cli/contract3.ts +282 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +89 -14
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +5 -2
- package/src/cli/runtime/open.ts +63 -98
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/stores.ts +53 -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 +34 -18
- package/src/cli/workspace-migrate.ts +125 -16
- package/src/connectivity/manifest/provider.ts +3 -1
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +36 -27
- package/src/deployments/gcp/bucket.ts +18 -7
- package/src/deployments/gcp/provision.ts +7 -7
- 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 +15 -15
- package/src/deployments/upload.ts +46 -22
- 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 +183 -0
- package/src/profile/deployments.ts +3 -3
- package/src/profile/index.ts +30 -5
- package/src/profile/layout.ts +86 -89
- package/src/profile/load.ts +80 -47
- 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 +172 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +69 -2
- package/src/profile/workspace.ts +58 -3
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/identity/provider.ts +1 -1
- package/src/providers/memory/provider.ts +20 -2
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/slack/index.ts +2 -2
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +85 -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 +1 -1
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/tools.ts +9 -3
- package/src/server/mcp/visibility.ts +18 -3
- 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
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
return declared?.vault?.ref ?? `vault/${soleGrantFor(config, 'vault') ?? 'main'}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A connection whose provider id moved out from under it.
|
|
161
|
+
*
|
|
162
|
+
* `tasks` is the built-in task list; Google's is `google_tasks` (ADR-051). A row
|
|
163
|
+
* that still says `tasks` and names an address rather than the built-in's own
|
|
164
|
+
* label is somebody's Google account resolving to the wrong provider — and the
|
|
165
|
+
* failure is silent in the worst way: the row loads, reconcile calls it active,
|
|
166
|
+
* and every call goes to an empty local store.
|
|
167
|
+
*
|
|
168
|
+
* Here rather than in the profile loader, because the evidence is the *account*
|
|
169
|
+
* and accounts live in this file.
|
|
170
|
+
*/
|
|
171
|
+
export function assertNoRenamedProviders(
|
|
172
|
+
connections: readonly ConnectionConfig[],
|
|
173
|
+
repair: string,
|
|
174
|
+
): void {
|
|
175
|
+
const problems = connections.flatMap((connection, index) => {
|
|
176
|
+
const renamed = describeRename(connection, repair);
|
|
177
|
+
return renamed === null ? [] : [`connections[${index}]: ${renamed}`];
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (problems.length > 0) {
|
|
181
|
+
throw new ConfigError(`connections.yaml:\n${problems.map((p) => ` ${p}`).join('\n')}`, problems);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -78,12 +78,12 @@ async function editRegistry(
|
|
|
78
78
|
(await readWorkspaceFile(files, WORKSPACE_FILE)) ?? `contract: ${SUPPORTED_CONTRACT}\n`;
|
|
79
79
|
|
|
80
80
|
const document = parseDocument(text);
|
|
81
|
-
const targets = (document.toJSON()?.
|
|
81
|
+
const targets = (document.toJSON()?.workspaces ?? {}) as Record<string, WorkspaceTarget>;
|
|
82
82
|
|
|
83
83
|
edit(targets);
|
|
84
84
|
|
|
85
|
-
if (Object.keys(targets).length === 0) document.deleteIn(['
|
|
86
|
-
else document.setIn(['
|
|
85
|
+
if (Object.keys(targets).length === 0) document.deleteIn(['workspaces']);
|
|
86
|
+
else document.setIn(['workspaces'], sorted(targets));
|
|
87
87
|
|
|
88
88
|
// Validated before it lands, on the rendered tree rather than the input, so
|
|
89
89
|
// what is checked is what would be read back.
|
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,21 +19,41 @@ 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,
|
|
35
59
|
knowledgeRoot,
|
|
@@ -103,6 +127,8 @@ export {
|
|
|
103
127
|
writeWorkspaceFile,
|
|
104
128
|
} from './files.ts';
|
|
105
129
|
export {
|
|
130
|
+
CONNECTIONS_FILE,
|
|
131
|
+
readConnections,
|
|
106
132
|
type ProfileSelection,
|
|
107
133
|
type Resolution,
|
|
108
134
|
type ResolveOptions,
|
|
@@ -111,5 +137,4 @@ export {
|
|
|
111
137
|
export {
|
|
112
138
|
DATA_DIR,
|
|
113
139
|
layout,
|
|
114
|
-
profileDir,
|
|
115
140
|
} from './layout.ts';
|
package/src/profile/layout.ts
CHANGED
|
@@ -1,123 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Where a
|
|
2
|
+
* Where a workspace's data lives, in one place.
|
|
3
3
|
*
|
|
4
4
|
* ```
|
|
5
5
|
* ~/.lanes-link/
|
|
6
|
-
* ├── lanes-link.yaml
|
|
7
|
-
* ├──
|
|
6
|
+
* ├── lanes-link.yaml the workspaces this machine knows, and the default
|
|
7
|
+
* ├── connections.yaml every account authorised in this workspace
|
|
8
|
+
* ├── profiles/<name>.yaml which of them a profile selects, and who may use it
|
|
8
9
|
* └── data/
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* └── <provider>/<connection>/… whatever that provider stores
|
|
10
|
+
* ├── state.kv/ state, connections, cursors
|
|
11
|
+
* ├── audit.log/ one object per event, one chain
|
|
12
|
+
* ├── credentials.enc system credentials, and its .key
|
|
13
|
+
* ├── vault.d/<id>.enc one sealed document per vault connection
|
|
14
|
+
* ├── skills.d/<id>/ procedures, one <name>/SKILL.md each
|
|
15
|
+
* ├── providers.d/ the operator's own provider manifests
|
|
16
|
+
* └── <provider>/<connection>/… whatever that provider stores
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
|
-
* **
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* **Nothing here takes a profile.** It used to take one for everything: a
|
|
20
|
+
* profile owned a directory, and `rm -r data/work` was the whole answer to
|
|
21
|
+
* "remove the work profile's data". A connection belongs to the workspace now
|
|
22
|
+
* (ADR-057) and so do the stores behind the owner layer (ADR-059), so a profile
|
|
23
|
+
* owns no bytes at all — it owns a selection, and the thing a selection points
|
|
24
|
+
* at outlives it. `profile remove` prints what survives rather than letting the
|
|
25
|
+
* difference go unnoticed.
|
|
25
26
|
*
|
|
26
|
-
* **The
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
27
|
+
* **The dot is load-bearing, and it is the only thing keeping these apart from
|
|
28
|
+
* a provider.** The blob root is `data/` and a provider is namespaced
|
|
29
|
+
* `<provider>/<connection>` under it; a provider id is `[a-z][a-z0-9_]*`, so a
|
|
30
|
+
* name carrying a dot is one no provider can be scoped into. That is not
|
|
31
|
+
* hypothetical for three of the five names below — `skills`, `vault` and
|
|
32
|
+
* `audit` are all real provider ids, and without the dot each would be handed a
|
|
33
|
+
* store rooted inside the thing it is meant to be walled off from, which is a
|
|
34
|
+
* hole in ADR-007's wall rather than an untidy filename.
|
|
34
35
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* be handed, and ADR-014 §1 already treats authoring one as a grant worth
|
|
40
|
-
* governing — an odd thing to say about a document every profile reads anyway.
|
|
41
|
-
* And a procedure written for work names work's accounts, its people, and its
|
|
42
|
-
* conventions, so it is exactly as private as the knowledge it operates on.
|
|
43
|
-
* ADR-030 has the argument; ADR-009's "profiles share nothing" is what it
|
|
44
|
-
* restores.
|
|
36
|
+
* `vault.d/<id>.enc` rather than `vault/<id>.enc` for exactly that reason. The
|
|
37
|
+
* sealed document is not a blob the vault provider serves, and a `vault/`
|
|
38
|
+
* prefix shared between the two would put the ciphertext inside the namespace
|
|
39
|
+
* the provider is given.
|
|
45
40
|
*
|
|
46
|
-
*
|
|
47
|
-
* `data/<profile>/skills/` is precisely the namespace the skills provider's own
|
|
48
|
-
* blobs scope into, so the one name that could not be used is the obvious one.
|
|
49
|
-
*
|
|
50
|
-
* These are **defaults**. A profile that declares its own paths keeps them.
|
|
51
|
-
*
|
|
52
|
-
* There is no migration from the layout this replaced, deliberately: a
|
|
53
|
-
* workspace is profiles, credentials, and whatever the owner has stored, and
|
|
54
|
-
* re-creating one is `lanes link profile add` plus `lanes link connect` per account. Machinery
|
|
55
|
-
* to move an old one would be more code than the thing it moves, and it would
|
|
56
|
-
* have to keep working forever to be worth having. Skills and manifests left at
|
|
57
|
-
* the old workspace-root paths are the same case: they stop loading, and moving
|
|
58
|
-
* them is one `mv` per profile that should see them.
|
|
41
|
+
* These are **defaults**. A workspace that declares its own paths keeps them.
|
|
59
42
|
*/
|
|
60
43
|
|
|
61
|
-
/** The directory under the workspace root that holds
|
|
44
|
+
/** The directory under the workspace root that holds everything the workspace owns. */
|
|
62
45
|
export const DATA_DIR = 'data';
|
|
63
46
|
|
|
64
47
|
/**
|
|
65
|
-
*
|
|
48
|
+
* No leading `./` on any of these.
|
|
66
49
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
50
|
+
* It used to carry one, which `path.resolve` discards and an object key does
|
|
51
|
+
* not: a bucket read `./data/state.kv/x` as a directory literally named `.`, so
|
|
52
|
+
* every deployed key landed one level away from where the config said it did.
|
|
53
|
+
* The visible cost was that the conditioned IAM binding
|
|
54
|
+
* `deployments/gcp/provision.ts` writes — which grants writes under
|
|
72
55
|
* `objects/data/` — matched nothing, and the first revision 403'd on its boot
|
|
73
56
|
* reconcile. A relative path is relative without being spelled that way.
|
|
74
57
|
*/
|
|
75
|
-
export function profileDir(profile: string): string {
|
|
76
|
-
return `${DATA_DIR}/${profile}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
58
|
export const layout = {
|
|
59
|
+
/** Connections, provider state, and cursors: one object per key. */
|
|
60
|
+
state: (): string => `${DATA_DIR}/state.kv`,
|
|
61
|
+
/**
|
|
62
|
+
* System credentials — OAuth refresh tokens, the CI token. Never reachable
|
|
63
|
+
* from MCP.
|
|
64
|
+
*
|
|
65
|
+
* One store for the workspace, where there used to be one per profile. That
|
|
66
|
+
* is what makes a `credential_ref` unique by construction and retires
|
|
67
|
+
* `collidingRefs`, the deploy preflight that existed because two profiles
|
|
68
|
+
* deployed into one project shared this namespace and the collision was
|
|
69
|
+
* "silent until one profile is reading the other's account" (ADR-043,
|
|
70
|
+
* ADR-057).
|
|
71
|
+
*/
|
|
72
|
+
credentials: (): string => `${DATA_DIR}/credentials.enc`,
|
|
73
|
+
/** Every vault connection's sealed document lives under here. */
|
|
74
|
+
vaultRoot: (): string => `${DATA_DIR}/vault.d`,
|
|
75
|
+
/** One sealed document per vault connection, each under its own key. */
|
|
76
|
+
vault: (connection: string): string => `${DATA_DIR}/vault.d/${connection}.enc`,
|
|
80
77
|
/**
|
|
81
|
-
*
|
|
78
|
+
* The same document, keyed relative to the blob store.
|
|
82
79
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
80
|
+
* `blobs()` is already rooted at `data/`, so a blob adapter handed the path
|
|
81
|
+
* above would write `data/data/vault.d/...`. Two spellings of one location is
|
|
82
|
+
* exactly what this file exists to prevent, so the second one lives here
|
|
83
|
+
* beside the first rather than being assembled at the call site.
|
|
87
84
|
*/
|
|
88
|
-
|
|
89
|
-
/** System credentials — OAuth tokens, the profile token. Never reachable from MCP. */
|
|
90
|
-
credentials: (profile: string): string => `${profileDir(profile)}/credentials.enc`,
|
|
91
|
-
/** The owner's own items, under their own key. */
|
|
92
|
-
vault: (profile: string): string => `${profileDir(profile)}/vault.enc`,
|
|
85
|
+
vaultKey: (connection: string): string => `vault.d/${connection}.enc`,
|
|
93
86
|
/**
|
|
94
|
-
*
|
|
87
|
+
* Every skills connection's procedures live under here.
|
|
95
88
|
*
|
|
96
|
-
* The
|
|
97
|
-
*
|
|
98
|
-
*
|
|
89
|
+
* The root is exported beside the per-connection path because `deploy` needs
|
|
90
|
+
* to recognise the *area* without knowing which connections exist — and
|
|
91
|
+
* `upload.ts` composes its allowlist back out of these rather than comparing
|
|
92
|
+
* against literals, so a renamed directory moves the store and the thing that
|
|
93
|
+
* sends it together, or neither.
|
|
99
94
|
*/
|
|
100
|
-
|
|
95
|
+
skillsRoot: (): string => `${DATA_DIR}/skills.d`,
|
|
96
|
+
/** One skills connection's procedures — `<name>.md` or `<name>/SKILL.md`, either layout. */
|
|
97
|
+
skills: (connection: string): string => `${DATA_DIR}/skills.d/${connection}`,
|
|
101
98
|
/**
|
|
102
|
-
* The provider manifests this
|
|
99
|
+
* The provider manifests this workspace declares.
|
|
103
100
|
*
|
|
104
|
-
*
|
|
105
|
-
* an OpenAPI document, and the credential refs that reach them — which
|
|
106
|
-
*
|
|
107
|
-
*
|
|
101
|
+
* Workspace-level, where ADR-030 put them in the profile. A manifest names a
|
|
102
|
+
* host, an OpenAPI document, and the credential refs that reach them — which
|
|
103
|
+
* is to say it *defines a connection*, and connections do not live in a
|
|
104
|
+
* profile any more. ADR-030's argument was about a procedure being as private
|
|
105
|
+
* as the knowledge it operates on; that argument is answered by ADR-059's
|
|
106
|
+
* instances, not by where a manifest sits.
|
|
108
107
|
*/
|
|
109
|
-
providers: (
|
|
108
|
+
providers: (): string => `${DATA_DIR}/providers.d`,
|
|
110
109
|
/** The blob root every provider is namespaced under. */
|
|
111
|
-
blobs: (
|
|
110
|
+
blobs: (): string => DATA_DIR,
|
|
112
111
|
/**
|
|
113
|
-
* The audit log: one object per event,
|
|
112
|
+
* The audit log: one object per event, one chain for the workspace.
|
|
114
113
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* rooted inside the log, which is a hole in ADR-007's wall rather than an
|
|
120
|
-
* untidy filename.
|
|
114
|
+
* One chain rather than one per profile, because one endpoint serves them all
|
|
115
|
+
* (ADR-009) and every event already records the profile it acted in. It is
|
|
116
|
+
* also what lets `audit tail` filter where it used to select, and what gives
|
|
117
|
+
* the dashboard a single log to read.
|
|
121
118
|
*/
|
|
122
|
-
audit: (
|
|
119
|
+
audit: (): string => `${DATA_DIR}/audit.log`,
|
|
123
120
|
} as const;
|