@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the dashboard's pairing credentials live in a workspace's store.
|
|
3
|
+
*
|
|
4
|
+
* Here rather than beside the command that writes them, because three
|
|
5
|
+
* components now read these names and only one of them is the CLI: the server
|
|
6
|
+
* opens the read surface with them (`#server/read`), and a deploy binds the
|
|
7
|
+
* token so the revision may read it (`#deployments/prepare.ts`). Importing a
|
|
8
|
+
* *command* module for a string constant dragged `cli/output.ts`,
|
|
9
|
+
* `cli/prompt.ts` and the terminal handling behind them into the container's
|
|
10
|
+
* runtime graph, which is a large amount of the CLI to load in order to learn
|
|
11
|
+
* three names.
|
|
12
|
+
*
|
|
13
|
+
* Underscores, not dots. A secret reference is `[a-z0-9_-]` separated by `/`
|
|
14
|
+
* (see `isValidSecretRef`), and that is not arbitrary: these names become
|
|
15
|
+
* Secret Manager entries on a deployed workspace, and Google allows no dots
|
|
16
|
+
* there either. `workspace/pair.cert` was refused at the moment somebody first
|
|
17
|
+
* ran the command.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The credential the dashboard presents. Reads everything; can change nothing. */
|
|
21
|
+
export const PAIR_TOKEN_REF = 'workspace/pair_token';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The certificate the loopback read listener terminates TLS with, and its key.
|
|
25
|
+
*
|
|
26
|
+
* Loopback only, and deliberately not bound on a deployed workspace: Cloud Run
|
|
27
|
+
* terminates TLS with a certificate a browser already trusts, so a deployed
|
|
28
|
+
* revision never calls `serveRead` and never reads either of these. Binding two
|
|
29
|
+
* secrets nothing reads would say the boundary is wider than it is.
|
|
30
|
+
*/
|
|
31
|
+
export const PAIR_CERT_REF = 'workspace/pair_cert';
|
|
32
|
+
export const PAIR_KEY_REF = 'workspace/pair_key';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The string shapes the config contract is built out of.
|
|
5
5
|
*
|
|
6
6
|
* Their own file because more than one schema module needs them, and the
|
|
7
7
|
* alternative was either a circular import or a second copy of a regex that
|
|
@@ -66,3 +66,37 @@ export const browserOrigin = z.string().refine(
|
|
|
66
66
|
},
|
|
67
67
|
'must be "*" or an origin with no trailing slash or path, e.g. "https://chat.example"',
|
|
68
68
|
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `<provider>.<connection>` — how a grant names what it governs.
|
|
72
|
+
*
|
|
73
|
+
* One string rather than two fields, because it is one string everywhere else
|
|
74
|
+
* it appears: the `connection` argument an agent passes, the audit event, the
|
|
75
|
+
* `setup_overview` listing, and the refusal a mismatched pairing produces.
|
|
76
|
+
* Splitting it in the config alone would mean every reader joins it back
|
|
77
|
+
* together.
|
|
78
|
+
*/
|
|
79
|
+
export const connectionRef = z
|
|
80
|
+
.string()
|
|
81
|
+
.regex(
|
|
82
|
+
/^[a-z][a-z0-9_]*\.[a-z0-9][a-z0-9_]*$/,
|
|
83
|
+
'must be "<provider>.<connection>", e.g. "gmail.personal"',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Who a profile is delegated to: `lanes:<subject>` (ADR-060).
|
|
88
|
+
*
|
|
89
|
+
* The prefix is load-bearing twice over, and the second reason is the one that
|
|
90
|
+
* would otherwise be found the hard way. A Lanes subject is a 28-character
|
|
91
|
+
* mixed-case alphanumeric string — which is exactly what `secret-detection.ts`
|
|
92
|
+
* refuses as a high-entropy blob, so a bare one could not be written into a
|
|
93
|
+
* profile at all. The colon takes the value out of `OPAQUE_TOKEN`'s character
|
|
94
|
+
* class, so saying which identity provider vouched for the subject and being
|
|
95
|
+
* storable are the same decision rather than an exemption list.
|
|
96
|
+
*
|
|
97
|
+
* A pasted credential still cannot be smuggled in here: it does not match this
|
|
98
|
+
* pattern either, which is what makes the fix a narrowing rather than a hole.
|
|
99
|
+
*/
|
|
100
|
+
export const subjectRef = z
|
|
101
|
+
.string()
|
|
102
|
+
.regex(/^lanes:[A-Za-z0-9]{6,64}$/, 'must be "lanes:<subject>", as written by lanes auth login');
|
package/src/profile/registry.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
* A machine reaches a target it does not hold through a **pointer** — a registry
|
|
28
28
|
* entry carrying `workspace:` and nothing else. Following one is a read of that
|
|
29
29
|
* workspace's own file, which is why every function here is async and why
|
|
30
|
-
* `--
|
|
30
|
+
* `--workspace cloud` needs the bucket reachable. That is the trade ADR-052 takes
|
|
31
31
|
* deliberately: a cloud target that cannot be read says so, where the shape it
|
|
32
32
|
* replaces answered instantly from a copy that had been wrong for eight hours.
|
|
33
33
|
*/
|
|
@@ -51,7 +51,7 @@ export interface ResolvedTarget {
|
|
|
51
51
|
/** Every target the workspace at `root` knows about. Empty when it has no file. */
|
|
52
52
|
export async function readRegistry(root: string): Promise<Record<string, WorkspaceTarget>> {
|
|
53
53
|
const workspace = await readWorkspace(root);
|
|
54
|
-
return workspace?.
|
|
54
|
+
return workspace?.workspaces ?? {};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -66,7 +66,7 @@ export async function resolveTargetWorkspace(root: string, target: string): Prom
|
|
|
66
66
|
const registry = await readRegistry(root);
|
|
67
67
|
const entry = registry[target];
|
|
68
68
|
if (!entry) throw notInRegistry(target, registry, root);
|
|
69
|
-
return isPointer(entry) ? entry.
|
|
69
|
+
return isPointer(entry) ? entry.at.replace(/\/$/, '') : root;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -90,7 +90,7 @@ export async function openTarget(root: string, target: string): Promise<Resolved
|
|
|
90
90
|
return { target, workspaceRoot: root, declared, entry, remote: false };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
const workspaceRoot = entry.
|
|
93
|
+
const workspaceRoot = entry.at.replace(/\/$/, '');
|
|
94
94
|
const remoteRegistry = await readRegistry(workspaceRoot);
|
|
95
95
|
const remoteEntry = remoteRegistry[target];
|
|
96
96
|
|
|
@@ -146,7 +146,7 @@ function pointerMissesTarget(
|
|
|
146
146
|
return new ConfigError(
|
|
147
147
|
`${root} says target "${target}" lives at ${workspaceRoot}, but that workspace does not ` +
|
|
148
148
|
`declare it (it declares: ${there}).\n` +
|
|
149
|
-
` Adopt what is really there: lanes link sync targets --
|
|
149
|
+
` Adopt what is really there: lanes link sync targets --workspace ${target} --from ${workspaceRoot}`,
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -176,7 +176,7 @@ function remoteAtContractOne(target: string, workspaceRoot: string): ConfigError
|
|
|
176
176
|
return new ConfigError(
|
|
177
177
|
`${workspaceRoot} is a contract 1 workspace, so it does not declare "${target}" yet.\n` +
|
|
178
178
|
' Its profiles still carry their own targets: block, which this version does not read.\n\n' +
|
|
179
|
-
` lanes link deploy --
|
|
179
|
+
` lanes link deploy --workspace ${target}\n` +
|
|
180
180
|
' migrates it and rolls the image that can read it, in that order — which is what\n' +
|
|
181
181
|
' keeps the endpoint in front of it serving throughout (ADR-052).',
|
|
182
182
|
);
|
package/src/profile/schema.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
browserOrigin,
|
|
4
|
+
capabilityPattern,
|
|
5
|
+
connectionRef,
|
|
6
|
+
credentialRef,
|
|
7
|
+
identifier,
|
|
8
|
+
subjectRef,
|
|
9
|
+
} from './primitives.ts';
|
|
3
10
|
import { authorizationSchema } from './authorization.ts';
|
|
4
11
|
import { identitySchema } from './identity.ts';
|
|
5
12
|
import { knowledgeTargetSchema } from './knowledge.ts';
|
|
@@ -35,11 +42,28 @@ import { knowledgeTargetSchema } from './knowledge.ts';
|
|
|
35
42
|
* workspace *is* a target: it declares its own adapters once, holds the profiles
|
|
36
43
|
* that live in it, and a profile is one copy in one place.
|
|
37
44
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
45
|
+
* **3 moved `connections:` out of the profile and into the workspace**
|
|
46
|
+
* (ADR-057), and replaced the profile's flat `policy:` with `grants:`, one row
|
|
47
|
+
* per connection (ADR-058). A profile stopped being an inventory of accounts
|
|
48
|
+
* and became a selection over them, which is what makes "read this mailbox,
|
|
49
|
+
* write that calendar" a thing that can be written down. It also gained
|
|
50
|
+
* `members:`, because a profile worth sharing needs to say who may consume it
|
|
51
|
+
* (ADR-060).
|
|
52
|
+
*
|
|
53
|
+
* **4 gave a profile its bytes back, and retired `data/`** (ADR-066, ADR-067).
|
|
54
|
+
* Contract 3 moved the owner layer's stores beside the *connection*, so a
|
|
55
|
+
* profile owned nothing and two profiles granting one memory read one note. The
|
|
56
|
+
* profile is the container again — `profiles/<name>/` holds its declaration and
|
|
57
|
+
* everything it owns — and `data/`, which had been the line between what a
|
|
58
|
+
* revision reads and what it writes, stopped drawing that line the moment the
|
|
59
|
+
* declaration moved inside it. No file's *shape* changed: `grants:`,
|
|
60
|
+
* `members:` and `connections.yaml` are untouched, and only the paths moved.
|
|
61
|
+
*
|
|
62
|
+
* A hard cut each time, and only the newest is read here. `./legacy.ts`
|
|
63
|
+
* understands the older shapes and only the migration uses it — a runtime that
|
|
64
|
+
* loaded either would be the two-sources-of-truth problem again, one level up.
|
|
41
65
|
*/
|
|
42
|
-
export const SUPPORTED_CONTRACT =
|
|
66
|
+
export const SUPPORTED_CONTRACT = 4;
|
|
43
67
|
|
|
44
68
|
/**
|
|
45
69
|
* There is no `database:` block any more.
|
|
@@ -136,7 +160,7 @@ export const auditTargetSchema = z.object({
|
|
|
136
160
|
*/
|
|
137
161
|
export const vaultTargetSchema = z.object({
|
|
138
162
|
adapter: z.enum(['file', 'blob', 'secret']),
|
|
139
|
-
/** File path, or blob key. Defaults to
|
|
163
|
+
/** File path, or blob key. Defaults to `data/vault.d/<connection>.enc`. */
|
|
140
164
|
path: z.string().optional(),
|
|
141
165
|
/** `secret` only: where the sealed document lives. Defaults to `vault/document`. */
|
|
142
166
|
ref: credentialRef.optional(),
|
|
@@ -327,6 +351,69 @@ export const policySchema = z.object({
|
|
|
327
351
|
deny: z.array(policyRuleSchema).default([]),
|
|
328
352
|
});
|
|
329
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Providers a profile may grant at most one instance of.
|
|
356
|
+
*
|
|
357
|
+
* Not a limitation of the store — `data/skills.d/<id>/` and
|
|
358
|
+
* `data/vault.d/<id>.enc` hold as many as anyone makes (ADR-059). It is a
|
|
359
|
+
* limitation of the surface, and it comes from the protocol rather than from
|
|
360
|
+
* here.
|
|
361
|
+
*
|
|
362
|
+
* Every other owner-layer tool takes a `connection` argument, so two memory
|
|
363
|
+
* instances are two routes through one tool and the caller says which. These
|
|
364
|
+
* two have nowhere to put that. A skill is surfaced as an MCP **prompt**, and a
|
|
365
|
+
* prompt is selected by name with no arguments to route on (ADR-012) — two
|
|
366
|
+
* `triage` skills in one profile would be one name for two procedures. A vault
|
|
367
|
+
* item becomes its own `vault.get.<id>` capability, and capability ids are flat
|
|
368
|
+
* for the same reason: two instances holding `stripe_key` would be one
|
|
369
|
+
* capability naming two secrets, which is the worst of the three collisions
|
|
370
|
+
* because the wrong answer is a credential.
|
|
371
|
+
*
|
|
372
|
+
* So the refusal is at load, where it names both rows, rather than at call time
|
|
373
|
+
* where one would silently win.
|
|
374
|
+
*/
|
|
375
|
+
export const SINGLE_INSTANCE_PROVIDERS: readonly string[] = ['lanes_skills', 'lanes_vault'];
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* One connection, and what may be done with it (ADR-058).
|
|
379
|
+
*
|
|
380
|
+
* The rules are the same `allow`/`deny` pair `policySchema` has always carried;
|
|
381
|
+
* what is new is that a row names the account they govern. ADR-003 kept rules
|
|
382
|
+
* connection-blind on the reasoning that "a narrower grant is a narrower
|
|
383
|
+
* profile" — sound while a connection lived in exactly one profile, because the
|
|
384
|
+
* second profile *was* the granularity. ADR-057 removed that mechanism, so the
|
|
385
|
+
* granularity moves into the rule.
|
|
386
|
+
*
|
|
387
|
+
* **There is no profile-wide `allow` beside these rows, deliberately.** A
|
|
388
|
+
* second place a connection could be granted is a second answer to "may this
|
|
389
|
+
* call proceed", and two answers to one question is the failure ADR-052 exists
|
|
390
|
+
* to prevent. A profile that wants the same rules on eight connections writes
|
|
391
|
+
* eight rows; the repetition is the cost of there being one place to look.
|
|
392
|
+
*
|
|
393
|
+
* Default deny is unchanged and now bites one step earlier: a connection with
|
|
394
|
+
* no row is not reachable, and is never advertised.
|
|
395
|
+
*/
|
|
396
|
+
export const grantSchema = z.object({
|
|
397
|
+
connection: connectionRef,
|
|
398
|
+
allow: z.array(policyRuleSchema).default([]),
|
|
399
|
+
deny: z.array(policyRuleSchema).default([]),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Who may consume this profile (ADR-060).
|
|
404
|
+
*
|
|
405
|
+
* `role` decides who may edit this list and nothing else. Both roles reach
|
|
406
|
+
* exactly what the grants above allow, because a role that changed what an
|
|
407
|
+
* agent could call would be a second policy system beside `grants:`, with
|
|
408
|
+
* precedence rules between them, answering a question the first one already
|
|
409
|
+
* answers. Two people needing different scopes is two profiles, which is cheap
|
|
410
|
+
* now that neither of them re-authorises an account.
|
|
411
|
+
*/
|
|
412
|
+
export const memberSchema = z.object({
|
|
413
|
+
subject: subjectRef,
|
|
414
|
+
role: z.enum(['owner', 'member']).default('member'),
|
|
415
|
+
});
|
|
416
|
+
|
|
330
417
|
/**
|
|
331
418
|
* One authorised account.
|
|
332
419
|
*
|
|
@@ -412,15 +499,36 @@ export const configSchema = z.object({
|
|
|
412
499
|
})
|
|
413
500
|
.default({ requests_per_minute: 120, upstream_calls_per_minute: 60 }),
|
|
414
501
|
|
|
415
|
-
|
|
502
|
+
/**
|
|
503
|
+
* What this profile is for, in the owner's own words.
|
|
504
|
+
*
|
|
505
|
+
* New in contract 3, and it earns its place because a profile is now a thing
|
|
506
|
+
* you hand to somebody (ADR-060). "personal-assistant" is a name; "reads my
|
|
507
|
+
* mail, keeps the calendar, never sends" is what a member needs to know
|
|
508
|
+
* before accepting it. The dashboard and `setup_overview` show it.
|
|
509
|
+
*/
|
|
510
|
+
description: z.string().min(1).optional(),
|
|
416
511
|
|
|
417
512
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
513
|
+
* The connections this profile selects, and what may be done with each.
|
|
514
|
+
*
|
|
515
|
+
* There is no `connections:` block here any more — a connection belongs to
|
|
516
|
+
* the workspace (ADR-057), and `connections.yaml` beside this file is where
|
|
517
|
+
* it lives. A grant naming a connection the workspace does not hold is
|
|
518
|
+
* refused by `assertReferentialIntegrity` rather than loading and reaching
|
|
519
|
+
* nothing.
|
|
421
520
|
*/
|
|
422
|
-
|
|
423
|
-
|
|
521
|
+
grants: z.array(grantSchema).default([]),
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Who may consume this profile (ADR-060).
|
|
525
|
+
*
|
|
526
|
+
* Empty is not "everyone" — it is nobody, which is default deny applied to
|
|
527
|
+
* the identity axis. `profile add` writes the signed-in subject as `owner`,
|
|
528
|
+
* so the empty state is one a hand-edit produces rather than one anybody is
|
|
529
|
+
* handed.
|
|
530
|
+
*/
|
|
531
|
+
members: z.array(memberSchema).default([]),
|
|
424
532
|
|
|
425
533
|
/**
|
|
426
534
|
* Memory and skills, somewhere other than the target's own storage (ADR-041).
|
|
@@ -446,8 +554,35 @@ export const configSchema = z.object({
|
|
|
446
554
|
identity: identitySchema.default([]),
|
|
447
555
|
});
|
|
448
556
|
|
|
557
|
+
/**
|
|
558
|
+
* `connections.yaml` — every account authorised in this workspace (ADR-057).
|
|
559
|
+
*
|
|
560
|
+
* Its own file rather than a block in `lanes-link.yaml`, because that file is
|
|
561
|
+
* the registry and is read before anything else can be: resolving which
|
|
562
|
+
* workspace a command means must not require parsing every connection in it.
|
|
563
|
+
* And its own file rather than staying in the profile, because authorising an
|
|
564
|
+
* account and deciding what may be done with it are two acts, and only the
|
|
565
|
+
* second is a property of a profile.
|
|
566
|
+
*
|
|
567
|
+
* `oauth_apps` comes with them. A registered client belongs to the account it
|
|
568
|
+
* authenticates, not to whichever selection happens to name that account.
|
|
569
|
+
*
|
|
570
|
+
* There is still no `providers` block. A provider is enabled by having a
|
|
571
|
+
* connection to it — a second place to say so could only ever disagree with the
|
|
572
|
+
* first, and everything else a provider needs is in its manifest.
|
|
573
|
+
*/
|
|
574
|
+
export const connectionsFileSchema = z.object({
|
|
575
|
+
contract: z.number().int().positive(),
|
|
576
|
+
connections: z.array(connectionSchema).default([]),
|
|
577
|
+
oauth_apps: z.record(identifier, oauthAppSchema).default({}),
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
export type ConnectionsFile = z.infer<typeof connectionsFileSchema>;
|
|
581
|
+
|
|
449
582
|
export type Config = z.infer<typeof configSchema>;
|
|
450
583
|
export type ConnectionConfig = z.infer<typeof connectionSchema>;
|
|
584
|
+
export type GrantConfig = z.infer<typeof grantSchema>;
|
|
585
|
+
export type MemberConfig = z.infer<typeof memberSchema>;
|
|
451
586
|
export type PolicyRuleConfig = z.infer<typeof policyRuleSchema>;
|
|
452
587
|
export type TargetConfig = z.infer<typeof targetSchema>;
|
|
453
588
|
export type DeployConfig = z.infer<typeof deployTargetSchema>;
|
|
@@ -482,8 +617,14 @@ export type { IdentityEntry } from './identity.ts';
|
|
|
482
617
|
*/
|
|
483
618
|
export const workspaceTargetSchema = z
|
|
484
619
|
.object({
|
|
485
|
-
/**
|
|
486
|
-
|
|
620
|
+
/**
|
|
621
|
+
* A pointer: where the workspace declaring this one lives.
|
|
622
|
+
*
|
|
623
|
+
* Spelled `at:` rather than `workspace:` since contract 3. The block is
|
|
624
|
+
* `workspaces:` now (ADR-061), and `workspaces.cloud.workspace` names the
|
|
625
|
+
* concept twice and reads as a typo.
|
|
626
|
+
*/
|
|
627
|
+
at: z.string().min(1).optional(),
|
|
487
628
|
credentials: credentialsTargetSchema.optional(),
|
|
488
629
|
audit: auditTargetSchema.optional(),
|
|
489
630
|
storage: storageTargetSchema.optional(),
|
|
@@ -497,6 +638,15 @@ export const workspaceTargetSchema = z
|
|
|
497
638
|
* gets in — the one question about a deployment that must not be guessed at.
|
|
498
639
|
*/
|
|
499
640
|
primary: identifier.optional(),
|
|
641
|
+
/**
|
|
642
|
+
* The Lanes workspace this one serves, when it serves one (ADR-060).
|
|
643
|
+
*
|
|
644
|
+
* A binding, not an identity: it says whose membership list
|
|
645
|
+
* `profile members add` validates a subject against. Absent means the
|
|
646
|
+
* workspace delegates only to the signed-in owner, which is what `local`
|
|
647
|
+
* does.
|
|
648
|
+
*/
|
|
649
|
+
lanes_workspace: z.string().min(1).optional(),
|
|
500
650
|
last_deploy: z.string().optional(),
|
|
501
651
|
/**
|
|
502
652
|
* The CLI release that rolled the revision serving this target.
|
|
@@ -520,21 +670,21 @@ export const workspaceTargetSchema = z
|
|
|
520
670
|
// rather than preferring one: a pointer beside a declaration is two answers
|
|
521
671
|
// to "where are this target's bytes", and picking either silently is how the
|
|
522
672
|
// fifteen-connection bucket got reported as seven.
|
|
523
|
-
if (declares && entry.
|
|
673
|
+
if (declares && entry.at !== undefined) {
|
|
524
674
|
ctx.addIssue({
|
|
525
675
|
code: 'custom',
|
|
526
676
|
message:
|
|
527
|
-
'names
|
|
677
|
+
'names an "at" and also declares adapters — a workspace is declared in exactly ' +
|
|
528
678
|
'one workspace. Keep the adapters here, or keep the pointer and declare them there.',
|
|
529
679
|
});
|
|
530
680
|
return;
|
|
531
681
|
}
|
|
532
682
|
|
|
533
|
-
if (!declares && entry.
|
|
683
|
+
if (!declares && entry.at === undefined) {
|
|
534
684
|
ctx.addIssue({
|
|
535
685
|
code: 'custom',
|
|
536
686
|
message:
|
|
537
|
-
'declares neither "
|
|
687
|
+
'declares neither "at" nor "credentials" and "storage" — a workspace either ' +
|
|
538
688
|
'lives here or points at where it does.',
|
|
539
689
|
});
|
|
540
690
|
return;
|
|
@@ -559,7 +709,17 @@ export const workspaceTargetSchema = z
|
|
|
559
709
|
export const workspaceSchema = z.object({
|
|
560
710
|
contract: z.number().int().positive(),
|
|
561
711
|
default_profile: identifier.optional(),
|
|
562
|
-
|
|
712
|
+
/**
|
|
713
|
+
* The workspace a command acts in when `--workspace` is absent (ADR-061).
|
|
714
|
+
*
|
|
715
|
+
* The first key in this file that is read rather than parsed and ignored
|
|
716
|
+
* since ADR-037. What makes it not the resolution chain that decision removed
|
|
717
|
+
* is that there is one source, it is printed on every command that uses it,
|
|
718
|
+
* and every command that publishes or destroys refuses it outright. If the
|
|
719
|
+
* echo is ever dropped for tidiness, ADR-061 has been reversed.
|
|
720
|
+
*/
|
|
721
|
+
default_workspace: z.string().optional(),
|
|
722
|
+
workspaces: z.record(z.string(), workspaceTargetSchema).default({}),
|
|
563
723
|
});
|
|
564
724
|
|
|
565
725
|
export type WorkspaceConfig = z.infer<typeof workspaceSchema>;
|
|
@@ -568,8 +728,8 @@ export type WorkspaceTarget = z.infer<typeof workspaceTargetSchema>;
|
|
|
568
728
|
/** Whether a registry entry points elsewhere rather than declaring the target. */
|
|
569
729
|
export function isPointer(
|
|
570
730
|
entry: WorkspaceTarget,
|
|
571
|
-
): entry is WorkspaceTarget & {
|
|
572
|
-
return entry.
|
|
731
|
+
): entry is WorkspaceTarget & { at: string } {
|
|
732
|
+
return entry.at !== undefined;
|
|
573
733
|
}
|
|
574
734
|
|
|
575
735
|
/**
|
package/src/profile/targets.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { isPointer, type WorkspaceTarget } from './schema.ts';
|
|
|
16
16
|
* The chain this replaces resolved `--target`, then the variable, then the key,
|
|
17
17
|
* and printed which of the three it landed on. What that bought was one flag
|
|
18
18
|
* saved per command. What it cost was that an *ignored* flag still produced a
|
|
19
|
-
* working command — `profile add --
|
|
19
|
+
* working command — `profile add --workspace cloud` dropped the flag on the floor
|
|
20
20
|
* and the next command carried on from a different source, so the mistake
|
|
21
21
|
* surfaced one command later with nothing connecting it to its cause. A
|
|
22
22
|
* resolver with nowhere to fall back to cannot fail that way.
|
|
@@ -69,27 +69,39 @@ export function noTargetNamed(
|
|
|
69
69
|
registry: Registry,
|
|
70
70
|
root?: string,
|
|
71
71
|
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
|
72
|
+
options: { refusedDefault?: boolean } = {},
|
|
72
73
|
): ConfigError {
|
|
73
74
|
const names = Object.keys(registry).sort();
|
|
74
75
|
const where = root ?? 'this workspace';
|
|
75
76
|
|
|
76
77
|
if (names.length === 0) {
|
|
77
78
|
return new ConfigError(
|
|
78
|
-
`--
|
|
79
|
-
' Create one with: lanes link profile add <name> --
|
|
79
|
+
`--workspace is required, and ${where} declares none.\n` +
|
|
80
|
+
' Create one with: lanes link profile add <name> --workspace local',
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
const stale = env[LEGACY_TARGET_ENV];
|
|
84
85
|
|
|
86
|
+
// The refusal a *destructive* command gives is a different sentence, because
|
|
87
|
+
// the operator has a default set and is entitled to know why it was not used
|
|
88
|
+
// (ADR-061). Saying only "--workspace is required" to somebody who configured
|
|
89
|
+
// one reads as a bug in the tool.
|
|
90
|
+
const because =
|
|
91
|
+
options.refusedDefault === true
|
|
92
|
+
? "--workspace is required. This command publishes or destroys, so the\ndefault is not used for it.\n\n"
|
|
93
|
+
: "--workspace is required. This command opens a workspace's stores, and\nnothing else selects one.\n\n";
|
|
94
|
+
|
|
85
95
|
return new ConfigError(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
because +
|
|
97
|
+
` Workspaces in ${where}\n${rows(registry, names)}\n` +
|
|
98
|
+
`\n e.g. lanes link status --workspace ${names[0]}` +
|
|
99
|
+
(options.refusedDefault === true
|
|
100
|
+
? '\n\n A default is set and is used by every command that only reads.'
|
|
101
|
+
: '') +
|
|
90
102
|
(stale
|
|
91
103
|
? `\n\n ${LEGACY_TARGET_ENV}=${stale} is set in this shell and is no longer read.\n` +
|
|
92
|
-
' Unset it, or pass --
|
|
104
|
+
' Unset it, or pass --workspace.'
|
|
93
105
|
: ''),
|
|
94
106
|
);
|
|
95
107
|
}
|
|
@@ -126,7 +138,7 @@ function rows(registry: Registry, names: readonly string[]): string {
|
|
|
126
138
|
return names
|
|
127
139
|
.map((name) => {
|
|
128
140
|
const entry = registry[name]!;
|
|
129
|
-
if (isPointer(entry)) return ` ${name} ${entry.
|
|
141
|
+
if (isPointer(entry)) return ` ${name} ${entry.at}`;
|
|
130
142
|
const adapters = [entry.credentials?.adapter, entry.storage?.adapter]
|
|
131
143
|
.filter(Boolean)
|
|
132
144
|
.join(' ');
|
package/src/profile/testing.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function workspaceYaml(
|
|
|
58
58
|
return (
|
|
59
59
|
`contract: ${SUPPORTED_CONTRACT}\n` +
|
|
60
60
|
(options.defaultProfile ? `default_profile: ${options.defaultProfile}\n` : '') +
|
|
61
|
-
`
|
|
61
|
+
`workspaces:\n${blocks}\n`
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -73,6 +73,108 @@ export function pointerYaml(
|
|
|
73
73
|
return (
|
|
74
74
|
`contract: ${SUPPORTED_CONTRACT}\n` +
|
|
75
75
|
(options.defaultProfile ? `default_profile: ${options.defaultProfile}\n` : '') +
|
|
76
|
-
`
|
|
76
|
+
`workspaces:\n${local} ${target}:\n at: ${workspace}\n`
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A `connections.yaml` holding the owner layer plus whatever a test names.
|
|
82
|
+
*
|
|
83
|
+
* Every fixture that opens a runtime needs one now: a profile's grants name rows
|
|
84
|
+
* in this file, and `assertGrantsResolve` refuses a grant with nothing behind it
|
|
85
|
+
* (ADR-057). Thirty tests writing the seven owner-layer rows by hand is how a
|
|
86
|
+
* fixture ends up subtly different from what `newConnectionsTemplate` writes.
|
|
87
|
+
*/
|
|
88
|
+
export function connectionsYaml(
|
|
89
|
+
extra: readonly { id: string; provider: string; account: string }[] = [],
|
|
90
|
+
): string {
|
|
91
|
+
const owner = [
|
|
92
|
+
{ id: 'lan1', provider: 'lanes_memory', account: 'Memory' },
|
|
93
|
+
{ id: 'lan2', provider: 'lanes_tasks', account: 'Tasks' },
|
|
94
|
+
{ id: 'lan3', provider: 'lanes_assets', account: 'Assets' },
|
|
95
|
+
{ id: 'lan4', provider: 'lanes_skills', account: 'Skills' },
|
|
96
|
+
{ id: 'lan5', provider: 'lanes_vault', account: 'Vault' },
|
|
97
|
+
{ id: 'lan6', provider: 'lanes_setup', account: 'Setup' },
|
|
98
|
+
{ id: 'lan7', provider: 'lanes_entities', account: 'Entities' },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const rows = [...owner, ...extra]
|
|
102
|
+
.map((row) => ` - { id: ${row.id}, provider: ${row.provider}, account: ${row.account} }`)
|
|
103
|
+
.join('\n');
|
|
104
|
+
|
|
105
|
+
return `contract: ${SUPPORTED_CONTRACT}\nconnections:\n${rows}\noauth_apps: {}\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The owner-layer grant rows a fresh profile carries, plus whatever a test names. */
|
|
109
|
+
export function grantsYaml(
|
|
110
|
+
extra: readonly { connection: string; allow?: readonly string[]; deny?: readonly string[] }[] = [],
|
|
111
|
+
): string {
|
|
112
|
+
const owner = [
|
|
113
|
+
'lanes_memory',
|
|
114
|
+
'lanes_tasks',
|
|
115
|
+
'lanes_assets',
|
|
116
|
+
'lanes_skills',
|
|
117
|
+
'lanes_vault',
|
|
118
|
+
'lanes_setup',
|
|
119
|
+
'lanes_entities',
|
|
120
|
+
].map((provider, index) => ({
|
|
121
|
+
connection: `${provider}.lan${index + 1}`,
|
|
122
|
+
allow: [`${provider}.*`],
|
|
123
|
+
deny: [],
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
return [...owner, ...extra]
|
|
127
|
+
.map(
|
|
128
|
+
(grant) =>
|
|
129
|
+
` - { connection: ${grant.connection}, allow: [${(grant.allow ?? []).join(', ')}], ` +
|
|
130
|
+
`deny: [${(grant.deny ?? []).join(', ')}] }`,
|
|
131
|
+
)
|
|
132
|
+
.join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A whole profile document at the current contract, for a test that needs one. */
|
|
136
|
+
export function profileYaml(
|
|
137
|
+
profile: string,
|
|
138
|
+
options: {
|
|
139
|
+
port?: number;
|
|
140
|
+
grants?: readonly { connection: string; allow?: readonly string[]; deny?: readonly string[] }[];
|
|
141
|
+
members?: readonly string[];
|
|
142
|
+
} = {},
|
|
143
|
+
): string {
|
|
144
|
+
const members = (options.members ?? [])
|
|
145
|
+
.map((subject) => ` - { subject: ${subject}, role: owner }`)
|
|
146
|
+
.join('\n');
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
`contract: ${SUPPORTED_CONTRACT}\n` +
|
|
150
|
+
`instance:\n profile: ${profile}\n port: ${options.port ?? 7337}\n host: 127.0.0.1\n` +
|
|
151
|
+
`auth:\n mode: bearer\n token_ref: profile/token\n` +
|
|
152
|
+
`grants:\n${grantsYaml(options.grants ?? [])}\n` +
|
|
153
|
+
`members:${members ? `\n${members}` : ' []'}\n`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Write a profile's declaration into a fixture workspace.
|
|
159
|
+
*
|
|
160
|
+
* A profile is a directory now (ADR-067), so writing one means creating that
|
|
161
|
+
* directory — which every fixture that used to write `profiles/<name>.yaml`
|
|
162
|
+
* beside its siblings got for free. Centralised here rather than repeated in
|
|
163
|
+
* twenty test files, for the reason `layout.ts` exists at all: the last time a
|
|
164
|
+
* path was spelled in two places one of them went stale, and the failure was a
|
|
165
|
+
* listing that disagreed with a loader about what existed.
|
|
166
|
+
*/
|
|
167
|
+
export async function writeProfileFixture(
|
|
168
|
+
root: string,
|
|
169
|
+
profile: string,
|
|
170
|
+
body: string,
|
|
171
|
+
): Promise<string> {
|
|
172
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
173
|
+
const { dirname, join } = await import('node:path');
|
|
174
|
+
const { layout } = await import('./layout.ts');
|
|
175
|
+
|
|
176
|
+
const path = join(root, layout.profileConfig(profile));
|
|
177
|
+
await mkdir(dirname(path), { recursive: true });
|
|
178
|
+
await writeFile(path, body);
|
|
179
|
+
return path;
|
|
180
|
+
}
|