@oortmem/core 0.2.1 → 0.4.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/dist/grant.js CHANGED
@@ -1,40 +1,156 @@
1
+ import { addressOf, assertNode, authorOf, nodeKey, nodeOf, planetOf, sameAddress, sameAuthor, } from './address.js';
2
+ /**
3
+ * The agent id of a caller that named no agent (an MCP entry written before agents existed, a
4
+ * terminal without `--agent`). It reads and writes the person's Planet only: it has no Star, so
5
+ * it can neither see nor create one. No configured agent may use this id.
6
+ */
7
+ export const ANONYMOUS_AGENT = 'anonymous';
1
8
  /**
2
9
  * The permission every `MemoryStore` method takes. Opaque: built only by
3
- * `localGrant` or `principalGrant`, frozen, and nominal, so a raw
10
+ * `localGrant`, `principalGrant` or `nodeGrant`, frozen, and nominal, so a raw
4
11
  * `ScopeFilter` (or any look-alike object) is rejected by the type checker
5
12
  * and, for JavaScript callers, by `assertGrant` inside every store.
6
13
  *
7
- * `read` bounds what can be seen (`get`, `lexical`, `dense`, `list`,
8
- * `coldGet`, `recordAccess`); `write` bounds what can change (`upsert`,
9
- * `forget`, `setTier`, `expireMany`, `coldPut`). Cursors belong to the
10
- * principal. Anything outside a grant behaves exactly like something that
14
+ * `readNodes` bounds what can be seen (`get`, `lexical`, `dense`, `list`,
15
+ * `coldGet`, `recordAccess`); `writeNodes` bounds what can change (`upsert`,
16
+ * `forget`, `setTier`, `expireMany`, `coldPut`). A memory is covered when
17
+ * any pattern of the side covers its address and author. Cursors belong to
18
+ * the principal. Anything outside a grant behaves exactly like something that
11
19
  * does not exist.
12
20
  */
13
21
  export class Grant {
14
22
  #issued = true;
15
23
  principal;
24
+ /** @deprecated The pre-level view of `readNodes`; see `GrantFilter`. */
16
25
  read;
26
+ /** @deprecated The pre-level view of `writeNodes`; see `GrantFilter`. */
17
27
  write;
18
- constructor(principal, read, write) {
28
+ readNodes;
29
+ writeNodes;
30
+ /**
31
+ * The node new memories are written to (the home node): the principal's
32
+ * Planet unless the agent keeps a Star. Engine code that creates a memory
33
+ * (observe, remember) addresses it here; edits of existing memories keep
34
+ * their own address. Covered by `writeNodes` whenever they are not empty.
35
+ */
36
+ home;
37
+ constructor(principal, readNodes, writeNodes, home) {
19
38
  this.principal = Object.freeze({ ...principal });
20
- this.read = Object.freeze({ ...principal, projectIds: freezeProjects(read) });
21
- this.write = Object.freeze({ ...principal, projectIds: freezeProjects(write) });
39
+ this.readNodes = Object.freeze(readNodes.map(freezePattern));
40
+ this.writeNodes = Object.freeze(writeNodes.map(freezePattern));
41
+ this.home = Object.freeze(homeNode(principal, this.writeNodes, home));
42
+ this.read = Object.freeze({
43
+ ...principal,
44
+ projectIds: legacyProjects(principal, this.readNodes),
45
+ });
46
+ this.write = Object.freeze({
47
+ ...principal,
48
+ projectIds: legacyProjects(principal, this.writeNodes),
49
+ });
22
50
  Object.freeze(this);
23
51
  }
24
52
  /** @internal Use `localGrant` or `principalGrant`. */
25
- static issue(principal, read, write) {
26
- for (const k of ['orgId', 'customerId', 'agentId']) {
27
- if (typeof principal[k] !== 'string' || principal[k] === '') {
28
- throw new RangeError(`grant: ${k} must be a non-empty string`);
29
- }
30
- }
31
- return new Grant(principal, read, write);
53
+ static issue(principal, read, write, home = 'planet') {
54
+ checkPrincipal(principal);
55
+ const anonymous = principal.agentId === ANONYMOUS_AGENT;
56
+ if (anonymous && home === 'star')
57
+ throw new RangeError('grant: an anonymous agent has no Star');
58
+ const readSide = anonymous
59
+ ? [ownPlanet(principal, read)]
60
+ : [ownPlanet(principal, read), ownStar(principal, read)];
61
+ // An agent whose home is its Star may read the shared Planet but never change it: its writes
62
+ // (new memories, forget, pin, tiers, confirmations) stay in its Star.
63
+ const writeSide = home === 'star'
64
+ ? [ownStar(principal, write)]
65
+ : anonymous
66
+ ? [ownPlanet(principal, write)]
67
+ : [ownPlanet(principal, write), ownStar(principal, write)];
68
+ return new Grant(principal, readSide, writeSide, home === 'star' ? starNode(principal) : planetNode(principal));
69
+ }
70
+ /** @internal Use `nodeGrant` or `narrowGrant`. */
71
+ static issueNodes(principal, read, write, home) {
72
+ checkPrincipal(principal);
73
+ return new Grant(principal, read, write, home);
32
74
  }
33
75
  /** True for a real grant (not a copy, not a look-alike). */
34
76
  static is(x) {
35
77
  return typeof x === 'object' && x !== null && #issued in x;
36
78
  }
37
79
  }
80
+ function checkPrincipal(principal) {
81
+ for (const k of ['orgId', 'customerId', 'agentId']) {
82
+ if (typeof principal[k] !== 'string' || principal[k] === '') {
83
+ throw new RangeError(`grant: ${k} must be a non-empty string`);
84
+ }
85
+ }
86
+ }
87
+ /**
88
+ * The local grant side's Planet: the principal's own Planet, whichever of the
89
+ * person's agents wrote the memory (CON-62; before it, the pattern was
90
+ * restricted to the principal's own agent).
91
+ */
92
+ function ownPlanet(p, projectIds) {
93
+ return { node: planetNode(p), projectIds };
94
+ }
95
+ /** The principal's agent's own Star: private to that agent, never another's. */
96
+ function ownStar(p, projectIds) {
97
+ return { node: starNode(p), projectIds };
98
+ }
99
+ /** The Star of a principal's agent (owned by the principal's owner). */
100
+ export function starNode(p) {
101
+ return { level: 'agent', orgId: p.orgId, agentId: p.agentId };
102
+ }
103
+ /**
104
+ * `home` checked against the write side, or the default: the principal's
105
+ * Planet when the write side names it, else the first node written, else the
106
+ * Planet (a read-only grant writes nothing anyway).
107
+ */
108
+ function homeNode(p, write, home) {
109
+ if (home !== undefined) {
110
+ assertNode(home);
111
+ const { level: _l, ...rest } = home;
112
+ const node = { level: home.level, ...rest };
113
+ if (write.length > 0 && !write.some((w) => nodeKey(w.node) === nodeKey(node))) {
114
+ throw new RangeError('grant: home must be a node the grant writes');
115
+ }
116
+ return node;
117
+ }
118
+ const planet = planetNode(p);
119
+ if (write.length === 0 || write.some((w) => nodeKey(w.node) === nodeKey(planet)))
120
+ return planet;
121
+ return write[0].node;
122
+ }
123
+ function freezePattern(p) {
124
+ if (typeof p !== 'object' || p === null)
125
+ throw new TypeError('grant: a node pattern must be an object');
126
+ assertNode(p.node);
127
+ const { level: _l, ...rest } = p.node;
128
+ const node = Object.freeze({ level: p.node.level, ...rest });
129
+ if (p.authorAgentId !== undefined &&
130
+ (typeof p.authorAgentId !== 'string' || p.authorAgentId === '')) {
131
+ throw new RangeError('grant: authorAgentId must be a non-empty string');
132
+ }
133
+ return Object.freeze({
134
+ node,
135
+ projectIds: freezeProjects(p.projectIds),
136
+ ...(p.authorAgentId !== undefined && { authorAgentId: p.authorAgentId }),
137
+ });
138
+ }
139
+ /**
140
+ * The projects of `patterns` that a pre-level reader may assume: those of
141
+ * patterns naming exactly the principal's Planet and covering its agent's
142
+ * writing (no author restriction, or that agent). Never wider than the
143
+ * patterns themselves.
144
+ */
145
+ function legacyProjects(p, patterns) {
146
+ const own = nodeKey({ level: 'user', orgId: p.orgId, customerId: p.customerId });
147
+ const mine = patterns.filter((x) => (x.authorAgentId === undefined || x.authorAgentId === p.agentId) && nodeKey(x.node) === own);
148
+ if (mine.some((x) => x.projectIds === 'all'))
149
+ return 'all';
150
+ return Object.freeze([
151
+ ...new Set(mine.flatMap((x) => x.projectIds)),
152
+ ]);
153
+ }
38
154
  function freezeProjects(p) {
39
155
  if (p === 'all')
40
156
  return 'all';
@@ -51,9 +167,17 @@ function samePrincipal(a, b) {
51
167
  return a.orgId === b.orgId && a.customerId === b.customerId && a.agentId === b.agentId;
52
168
  }
53
169
  /**
54
- * The grant for today's single-principal callers: read exactly what `read`
55
- * names (a one-node grant), and write the same unless `opts.write` narrows it.
56
- * `write` must name the same principal as `read`.
170
+ * The grant for one agent of one person (CON-62): read and write the
171
+ * person's Planet (memories any of their agents wrote) and this agent's own
172
+ * Star, in the projects `read` names; `opts.write` may narrow the write side.
173
+ * Another agent's Star is never covered. `opts.home` picks where new memories
174
+ * land: the Planet (default) or the agent's Star; with `star` the Planet is
175
+ * read-only (the agent writes only its Star). `ANONYMOUS_AGENT` gets the
176
+ * Planet alone. `write` must name the same principal as `read`.
177
+ *
178
+ * Before CON-62 the Planet side was restricted to memories the principal's
179
+ * own agent wrote. Every local memory so far was written as `claude-code`,
180
+ * so a local store sees exactly what it saw; nothing is rewritten.
57
181
  */
58
182
  export function localGrant(read, opts = {}) {
59
183
  const write = opts.write ?? read;
@@ -64,27 +188,110 @@ export function localGrant(read, opts = {}) {
64
188
  if (!samePrincipal(read, write)) {
65
189
  throw new RangeError('localGrant: read and write must name the same org, customer and agent');
66
190
  }
67
- return Grant.issue(read, read.projectIds, write.projectIds);
191
+ return Grant.issue({ orgId: read.orgId, customerId: read.customerId, agentId: read.agentId }, read.projectIds, write.projectIds, opts.home);
192
+ }
193
+ /**
194
+ * Every project of `p` (and its global memories), for read and write:
195
+ * maintenance such as export. Covers the same nodes as `localGrant`: the
196
+ * person's Planet and this agent's own Star.
197
+ */
198
+ export function principalGrant(p, opts = {}) {
199
+ return Grant.issue({ orgId: p.orgId, customerId: p.customerId, agentId: p.agentId }, 'all', 'all', opts.home);
200
+ }
201
+ /**
202
+ * A grant over explicit node patterns, possibly across owners and levels:
203
+ * `read` bounds what is seen, `write` (default: `read`) what may change.
204
+ * Trusted: whoever builds one has already decided the principal may use
205
+ * every node it names (local configuration, or membership checked by a
206
+ * control plane). Nothing in the engine builds one for its own callers yet:
207
+ * the CLI, hooks and MCP server keep the pre-level grants.
208
+ */
209
+ export function nodeGrant(opts) {
210
+ if (!Array.isArray(opts.read) || (opts.write !== undefined && !Array.isArray(opts.write))) {
211
+ throw new TypeError('nodeGrant: read and write must be arrays of node patterns');
212
+ }
213
+ return Grant.issueNodes(opts.principal, opts.read, opts.write ?? opts.read, opts.home);
214
+ }
215
+ /**
216
+ * `g` narrowed to the nodes `keep` names (by `nodeKey`), on both sides: the
217
+ * grant recall uses for one node's first stage. Never wider than `g`. The
218
+ * home stays when it is kept, else the default for what remains.
219
+ */
220
+ export function narrowGrant(g, keep) {
221
+ assertGrant(g);
222
+ const keys = new Set(keep.map((n) => nodeKey(n)));
223
+ const read = g.readNodes.filter((p) => keys.has(nodeKey(p.node)));
224
+ const write = g.writeNodes.filter((p) => keys.has(nodeKey(p.node)));
225
+ const home = write.some((p) => nodeKey(p.node) === nodeKey(g.home)) ? g.home : undefined;
226
+ return Grant.issueNodes(g.principal, read, write, home);
227
+ }
228
+ /** The distinct nodes `g` reads, in the order its patterns name them. */
229
+ export function readableNodes(g) {
230
+ assertGrant(g);
231
+ const seen = new Map();
232
+ for (const p of g.readNodes)
233
+ if (!seen.has(nodeKey(p.node)))
234
+ seen.set(nodeKey(p.node), p.node);
235
+ return [...seen.values()];
68
236
  }
69
- /** Every project of `p` (and its global memories), for read and write: maintenance such as export. */
70
- export function principalGrant(p) {
71
- return Grant.issue(p, 'all', 'all');
237
+ /** The address a new memory gets under `g`: its home node, bound to `projectId` when given. */
238
+ export function homeAddress(g, projectId) {
239
+ assertGrant(g);
240
+ return {
241
+ ...g.home,
242
+ ...(projectId !== undefined && { projectId }),
243
+ };
72
244
  }
73
245
  /** Rejects anything that is not a real `Grant`; every store calls it first. */
74
246
  export function assertGrant(g) {
75
247
  if (!Grant.is(g)) {
76
- throw new TypeError('MemoryStore methods take a Grant (localGrant or principalGrant)');
248
+ throw new TypeError('MemoryStore methods take a Grant (localGrant, principalGrant or nodeGrant)');
77
249
  }
78
250
  }
79
- /** True iff a memory with `scope` lies inside `f`. Extra scope fields do not widen or narrow it. */
251
+ /**
252
+ * True iff a memory with `scope` lies inside `f`. Extra scope fields do not widen or narrow it.
253
+ * @deprecated Use `grantReads` / `grantWrites`.
254
+ */
80
255
  export function filterCovers(f, scope) {
81
256
  return (scope.orgId === f.orgId &&
82
257
  scope.customerId === f.customerId &&
83
258
  scope.agentId === f.agentId &&
84
259
  (f.projectIds === 'all' || f.projectIds.includes(scope.projectId ?? null)));
85
260
  }
86
- export const canRead = (g, scope) => filterCovers(g.read, scope);
87
- export const canWrite = (g, scope) => filterCovers(g.write, scope);
261
+ /** True iff `p` covers a memory at `address` written by `author`. */
262
+ export function patternCovers(p, address, author) {
263
+ return (nodeKey(nodeOf(address)) === nodeKey(p.node) &&
264
+ (p.projectIds === 'all' || p.projectIds.includes(address.projectId ?? null)) &&
265
+ (p.authorAgentId === undefined || p.authorAgentId === author.agentId));
266
+ }
267
+ function covers(patterns, m) {
268
+ const address = addressOf(m);
269
+ const author = authorOf(m);
270
+ return patterns.some((p) => patternCovers(p, address, author));
271
+ }
272
+ /** True iff `g` may read `m` (by its address and author). */
273
+ export const grantReads = (g, m) => covers(g.readNodes, m);
274
+ /** True iff `g` may change `m` (by its address and author). */
275
+ export const grantWrites = (g, m) => covers(g.writeNodes, m);
276
+ /** True iff `g` may read a Planet memory with `scope` (`scope.agentId` as its author). */
277
+ export const canRead = (g, scope) => grantReads(g, { scope });
278
+ /** True iff `g` may change a Planet memory with `scope` (`scope.agentId` as its author). */
279
+ export const canWrite = (g, scope) => grantWrites(g, { scope });
280
+ /**
281
+ * Two records occupy the same place: the same address, the same author and
282
+ * the same scope (extra scope fields included). A write that would change any
283
+ * of them for an existing id is refused.
284
+ */
285
+ export function sameRecordAddress(a, b) {
286
+ return (sameAddress(addressOf(a), addressOf(b)) &&
287
+ sameAuthor(authorOf(a), authorOf(b)) &&
288
+ sameScope(a.scope, b.scope));
289
+ }
290
+ /** The Planet node of a principal: what pre-level grants cover. */
291
+ export function planetNode(p) {
292
+ const { projectId: _p, ...node } = planetOf({ ...p, agentId: '-' });
293
+ return node;
294
+ }
88
295
  /**
89
296
  * The key a store keeps a cursor under. The local principal (the CLI's own,
90
297
  * `local`/`self`/`claude-code`) keeps the bare key, so cursors written before
package/dist/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
- export declare const VERSION = "0.2.1";
1
+ export declare const VERSION = "0.4.0";
2
2
  export * from './model.js';
3
+ export * from './address.js';
4
+ export * from './export-format.js';
3
5
  export * from './scope.js';
4
6
  export * from './config.js';
5
7
  export * from './activation.js';
6
8
  export * from './adapters.js';
7
9
  export * from './embed-cache.js';
8
10
  export * from './grant.js';
11
+ export * from './levels.js';
12
+ export * from './agents.js';
9
13
  export * from './fusion.js';
10
14
  export * from './memory-store.js';
11
15
  export * from './tokenize.js';
@@ -14,6 +18,7 @@ export * from './recall.js';
14
18
  export * from './relevance.js';
15
19
  export * from './observe.js';
16
20
  export * from './temporal.js';
21
+ export { chronoLoaded, mayHaveDateCue, preloadChrono } from './date-cue.js';
17
22
  export * from './volatile.js';
18
23
  export * from './scrub.js';
19
24
  export * from './cap.js';
package/dist/index.js CHANGED
@@ -1,11 +1,15 @@
1
- export const VERSION = '0.2.1';
1
+ export const VERSION = '0.4.0';
2
2
  export * from './model.js';
3
+ export * from './address.js';
4
+ export * from './export-format.js';
3
5
  export * from './scope.js';
4
6
  export * from './config.js';
5
7
  export * from './activation.js';
6
8
  export * from './adapters.js';
7
9
  export * from './embed-cache.js';
8
10
  export * from './grant.js';
11
+ export * from './levels.js';
12
+ export * from './agents.js';
9
13
  export * from './fusion.js';
10
14
  export * from './memory-store.js';
11
15
  export * from './tokenize.js';
@@ -14,6 +18,7 @@ export * from './recall.js';
14
18
  export * from './relevance.js';
15
19
  export * from './observe.js';
16
20
  export * from './temporal.js';
21
+ export { chronoLoaded, mayHaveDateCue, preloadChrono } from './date-cue.js';
17
22
  export * from './volatile.js';
18
23
  export * from './scrub.js';
19
24
  export * from './cap.js';
@@ -4,13 +4,26 @@
4
4
  * can require an empty list and a test can prove a broken store is caught.
5
5
  * No vitest here; `store-conformance.ts` wires these into `it` blocks.
6
6
  */
7
+ import { type Address, type Author } from './address.js';
7
8
  import type { MemoryStore } from './adapters.js';
8
9
  import { type Grant } from './grant.js';
9
10
  import type { Memory, Scope } from './model.js';
10
11
  /** A memory for the isolation checks: text, stub and entities all carry `marker`. */
11
12
  export declare function isoMemory(id: string, scope: Scope, marker: string, embedding?: Int8Array): Memory;
12
- /** The narrowest grant for one scope (its principal and its one project or global). */
13
+ /**
14
+ * The narrowest grant for one scope: its Planet, its one project (or global),
15
+ * memories its own agent wrote. (The pre-CON-62 local grant; today's
16
+ * `localGrant` also covers the Planet's other authors and the agent's Star.)
17
+ */
13
18
  export declare function soleGrant(s: Scope): Grant;
19
+ /** The local grant of `ISO_SCOPES.mine`'s agent for its one project: Planet plus its own Star. */
20
+ export declare function defaultGrant(s: Scope): Grant;
21
+ /** The narrowest grant for one memory at any level: its node, its one project, its author. */
22
+ export declare function soleGrantFor(m: Pick<Memory, 'scope' | 'address' | 'author'>): Grant;
23
+ /** `m` moved to `scope`: its address and author dropped, so the scope's Planet is the address. */
24
+ export declare function movedTo(m: Memory, scope: Scope): Memory;
25
+ /** A memory at `address`, written by `author`, for the isolation checks. */
26
+ export declare function isoAt(id: string, address: Address, author: Author, marker: string, embedding?: Int8Array): Memory;
14
27
  /** The scopes the checks use: the caller, and victims that differ from it in one coordinate each. */
15
28
  export declare const ISO_SCOPES: {
16
29
  mine: {
@@ -49,6 +62,23 @@ export declare const ISO_SCOPES: {
49
62
  projectId: string;
50
63
  };
51
64
  };
65
+ /**
66
+ * Victims at the other levels, each next to the caller's own Planet: a Planet
67
+ * memory another of the caller's agents wrote, the caller's agent's own Star,
68
+ * another agent's Star (in the caller's project and global), an Orbit, a
69
+ * Flyby, a Constellation and a Galaxy root of an organisation, and one of the
70
+ * caller's own owner. The narrowest grant sees none of them; the local grant
71
+ * sees only those in `DEFAULT_COVERS`.
72
+ */
73
+ export declare const ISO_LEVELS: Record<string, {
74
+ address: Address;
75
+ author: Author;
76
+ }>;
77
+ /**
78
+ * The victims the local grant of `ISO_SCOPES.mine` legitimately covers (CON-62): Planet memories
79
+ * the person's other agents wrote, and its agent's own Star. Never another agent's Star.
80
+ */
81
+ export declare const DEFAULT_COVERS: readonly string[];
52
82
  /**
53
83
  * Every `MemoryStore` method, called with a grant for `ISO_SCOPES.mine`
54
84
  * against memories (warm and cold) in each other scope: nothing is seen,
@@ -56,6 +86,10 @@ export declare const ISO_SCOPES: {
56
86
  * apart. Also the write-side rules (`GrantError` for an out-of-grant scope
57
87
  * and for an id that exists elsewhere, atomic batches, address changes),
58
88
  * read/write splits, cursor namespaces and rejection of non-grants.
89
+ *
90
+ * Run twice: as the narrowest grant (every victim out of reach), and as the
91
+ * local grant (`localGrant`, Planet plus own Star), for which every victim
92
+ * but `DEFAULT_COVERS` is out of reach, other agents' Stars included.
59
93
  */
60
94
  export declare function isolationViolations(make: () => Promise<MemoryStore>): Promise<string[]>;
61
95
  /** A small seeded PRNG (mulberry32), so a failing seed replays exactly. */
@@ -73,5 +107,17 @@ export declare function seededRng(seed: number): {
73
107
  * order: SQLite FTS5's bm25 uses table-wide term statistics, so another
74
108
  * tenant's text shifts scores (a known side channel; see the PR notes).
75
109
  * Every other result is compared exactly, dense scores included.
110
+ *
111
+ * `multiNode`: A's grant spans its Planet, its agent's Star and a
112
+ * Constellation of another owner; B's spans its own Planet and a sibling
113
+ * Constellation. Same property.
114
+ *
115
+ * `peers` (CON-62): A and B are two agents of one person, each with the local
116
+ * grant (the shared Planet plus its own Star). A writes to the Planet and its
117
+ * Star; B writes only to its own Star and aims every method at A's Star ids.
118
+ * Nothing B does through its Star reaches A.
76
119
  */
77
- export declare function nonInterference(make: () => Promise<MemoryStore>, seed: number, steps: number): Promise<string | null>;
120
+ export declare function nonInterference(make: () => Promise<MemoryStore>, seed: number, steps: number, opts?: {
121
+ multiNode?: boolean;
122
+ peers?: boolean;
123
+ }): Promise<string | null>;