@lanes-sh/link 0.9.1 → 0.9.2

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.
@@ -38,10 +38,10 @@ then a narrower fragment, before concluding something is not there.
38
38
  Tasks are worth the second look rather than an afterthought: an open one is often
39
39
  the actual answer to "what do we already know about X", and it carries something
40
40
  memory cannot — that the matter is unfinished. Say so when it is, and say which
41
- status: *blocked since June* is a different answer from *open*. `tasks.list`
41
+ status: *blocked since June* is a different answer from *open*. `lanes_tasks_list`
42
42
  hides finished work by default, so ask for `done` when the question is historical.
43
43
 
44
- **Assets are a listing, not a corpus.** `assets.list` tells you a file exists,
44
+ **Assets are a listing, not a corpus.** `lanes_assets_list` tells you a file exists,
45
45
  its type and its size; only a text one reads back. Report that a document is
46
46
  there and let the main thread decide what to do with it — do not try to get at
47
47
  the contents of a binary, and never ask for it as base64.
@@ -67,7 +67,7 @@ to every future session.
67
67
  stops a write is policy on the endpoint:
68
68
 
69
69
  ```console
70
- $ lanes link policy deny memory.write --connection memory.main --profile <name> --workspace <name>
70
+ $ lanes link policy deny lanes_memory.write --connection lanes_memory.lan1 --profile <name> --workspace <name>
71
71
  $ lanes link policy list --profile <name> --workspace <name>
72
72
  ```
73
73
 
@@ -43,6 +43,12 @@ workspace*. A profile lives in exactly one, so `personal` on `local` and
43
43
  `personal` on `cloud` are two profiles that share a name rather than one profile
44
44
  in two places.
45
45
 
46
+ **A profile owns its data.** Two profiles granting the same connection — every
47
+ profile grants `lanes_memory.lan1` — still read and write different notes,
48
+ tasks, files and entities, because the profile is part of where they are kept.
49
+ So there is nothing to be found in one by asking another, and a note taken under
50
+ `work` is not available under `personal`.
51
+
46
52
  **What a command must be told is never inferred from a profile — but the
47
53
  workspace may have a default.** `lanes set-workspace <name>` writes one, every
48
54
  command that uses it echoes the name it resolved, and the commands where being
@@ -74,12 +80,19 @@ disagree with it.
74
80
  When you write a command out for the owner, fill in what that command needs or
75
81
  leave it as `<name>` for them to complete — never drop a required one.
76
82
 
77
- A `connection` names an account the profile grants. One profile may grant
78
- several of the same kind and govern each differently, so `gmail.work` may be
79
- readable where `gmail.personal` is writable. Naming a connection the profile does
80
- not grant is refused rather than guessed at, and a connection it does not grant
81
- is absent from the enum entirely: if you cannot see it there, it was not
82
- withheld by accident.
83
+ A `connection` names an account the profile grants, and it is a **fully
84
+ qualified** `<provider>.<id>` `lanes_memory.lan1`, not `lan1`. The bare id is
85
+ refused, so take the value out of the enum rather than assembling one.
86
+
87
+ Ids are opaque: `con1`, `con2` for accounts and `lan1`, `lan2` for Lanes' own
88
+ surfaces. They carry no meaning and are not worth guessing at — the enum prints
89
+ each one's account and label beside it, and that is what tells `con1` from
90
+ `con2`. One profile may grant several of the same kind and govern each
91
+ differently, so `gmail.con1` may be readable where `gmail.con2` is writable.
92
+
93
+ Naming a connection the profile does not grant is refused rather than guessed
94
+ at, and one it does not grant is absent from the enum entirely: if you cannot
95
+ see it there, it was not withheld by accident.
83
96
 
84
97
  ## Which store a thing goes in
85
98
 
@@ -107,7 +120,7 @@ when Y", that is a skill they should write, not a memory entry describing it.
107
120
 
108
121
  ## Reach for memory before answering from nothing
109
122
 
110
- `lanes_memory.search` before concluding you do not know something about this person or
123
+ `lanes_memory_search` before concluding you do not know something about this person or
111
124
  their work. It is a substring search over their own notes, not a ranked index —
112
125
  try more than one wording before deciding it is not there.
113
126
 
@@ -118,7 +131,7 @@ entries with `lanes link memory list --profile <name> --workspace <name>` and a
118
131
 
119
132
  ## Tasks have a status, so finish them rather than deleting them
120
133
 
121
- `lanes_tasks.list` answers what is outstanding. It shows `in_progress`, `open` and
134
+ `lanes_tasks_list` answers what is outstanding. It shows `in_progress`, `open` and
122
135
  `blocked` and hides the rest, so a listing is what is left to do rather than
123
136
  everything that ever was — name a status to see more.
124
137
 
@@ -133,7 +146,7 @@ Six of them, and the two that are easy to confuse are worth learning:
133
146
  | `done` | finished |
134
147
  | `dropped` | decided against, which is not the same as finished |
135
148
 
136
- **Closing a task is `lanes_tasks.update` with a status, never `lanes_tasks.remove`.** The
149
+ **Closing a task is `lanes_tasks_update` with a status, never `lanes_tasks_remove`.** The
137
150
  record of having done it is the useful half, and it is what stops the same thing
138
151
  being suggested again next week. Remove is for something recorded by mistake.
139
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanes-sh/link",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "A self-hostable MCP gateway for all your connections, memory, tasks, files, and secrets",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://lanes.sh/link",
@@ -45,6 +45,41 @@ export interface McpAddOptions extends GlobalFlags {
45
45
  readonly headless?: boolean | undefined;
46
46
  }
47
47
 
48
+ /**
49
+ * What to say about the token, which depends on whether one was actually stored.
50
+ *
51
+ * **`token`, not `harness.storesToken`.** The harness property says the client
52
+ * *can* hold a token; whether one was passed is a different question, and only
53
+ * the headless path passes one — the ordinary path registers the bare URL and
54
+ * lets the client discover the protected-resource document and run the
55
+ * authorization itself. Keyed on the property, this told an operator who had
56
+ * just registered against a deployed endpoint that "the token was stored" and
57
+ * that a rotate meant re-registering, when nothing had been stored and a rotate
58
+ * would not touch that entry at all.
59
+ *
60
+ * A separate function so the decision can be tested without spawning a client
61
+ * binary, which is the only reason the branch above it cannot be.
62
+ */
63
+ export function tokenNote(
64
+ harness: { readonly storesToken: boolean; readonly label: string },
65
+ token: string | undefined,
66
+ ): readonly string[] {
67
+ if (!harness.storesToken) return [];
68
+
69
+ if (token) {
70
+ return [
71
+ 'The token was stored as a value, not a command, so "lanes link token rotate"',
72
+ 'means running this again with --force.',
73
+ ];
74
+ }
75
+
76
+ return [
77
+ `No token was stored: ${harness.label} reads the endpoint's own`,
78
+ 'protected-resource document and signs you in. A "lanes link token rotate"',
79
+ 'does not affect this registration.',
80
+ ];
81
+ }
82
+
48
83
  export async function mcpAdd(target: string | undefined, options: McpAddOptions): Promise<void> {
49
84
  // No harness named: every one that is actually installed. Registering with
50
85
  // whatever is present is what someone means by "add my mcp", and naming one
@@ -174,14 +209,7 @@ async function register(
174
209
 
175
210
  if (!options.noSkill) await installFor(harness, input.scope, {});
176
211
 
177
- if (harness.storesToken) {
178
- print(
179
- style.dim(
180
- ' The token was stored as a value, not a command, so "lanes link token rotate"\n' +
181
- ' means running this again with --force.',
182
- ),
183
- );
184
- }
212
+ for (const line of tokenNote(harness, input.token)) print(style.dim(` ${line}`));
185
213
 
186
214
  const after = harness.afterAdd?.(input);
187
215
  if (after) {
@@ -59,6 +59,25 @@ const TAIL_YEAR_WINDOW = 10;
59
59
 
60
60
  const MARKER_PREFIX = 'runs.closed/';
61
61
 
62
+ /**
63
+ * Whether a key is something this store wrote, rather than something the OS did.
64
+ *
65
+ * `verify` enumerates the whole prefix and feeds every non-marker key to the
66
+ * chain, so a `.DS_Store` that Finder dropped in the audit directory arrived as
67
+ * a record, failed to decode, and was reported as `malformed run ? at seq -1` —
68
+ * the whole log **BROKEN** because somebody opened the folder. Seen on a real
69
+ * workspace, where the file had ridden through two contract migrations.
70
+ *
71
+ * A dotfile is the narrowest rule that covers it: no key this store writes
72
+ * begins with a dot, in either segment. Deliberately not "does it look like an
73
+ * event" — a corrupt event must still fail loudly, because reporting `ok` for a
74
+ * record it could not read is the one thing `verify` must never do.
75
+ */
76
+ function isOurs(key: string): boolean {
77
+ return !key.split('/').some((segment) => segment.startsWith('.'));
78
+ }
79
+
80
+
62
81
  export interface BlobAuditOptions {
63
82
  /** Scoped to the audit root — `data/<profile>/audit.log` or its bucket prefix. */
64
83
  readonly storage: BlobStore;
@@ -113,7 +132,7 @@ export function createBlobAuditStore(options: BlobAuditOptions): AuditStore {
113
132
  for (let year = start; year >= floor && found.length < limit; year -= 1) {
114
133
  const keys = (await storage.list(`${year}/`))
115
134
  .map((entry) => entry.key)
116
- .filter((key) => !key.startsWith(MARKER_PREFIX))
135
+ .filter((key) => !key.startsWith(MARKER_PREFIX) && isOurs(key))
117
136
  // Keys are compact ISO within a day and the day is in the path, so
118
137
  // lexicographic order is chronological — no parsing to sort.
119
138
  .sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
@@ -160,6 +179,8 @@ export function createBlobAuditStore(options: BlobAuditOptions): AuditStore {
160
179
  const markers: RunMarker[] = [];
161
180
 
162
181
  for (const entry of await storage.list('')) {
182
+ if (!isOurs(entry.key)) continue;
183
+
163
184
  const bytes = await storage.get(entry.key);
164
185
  if (bytes === null) continue;
165
186