@oortmem/core 0.3.0 → 0.5.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/agents.d.ts +38 -0
- package/dist/agents.js +67 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +7 -0
- package/dist/consolidate.d.ts +36 -7
- package/dist/consolidate.js +114 -39
- package/dist/grant.d.ts +58 -11
- package/dist/grant.js +121 -23
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/isolation.d.ts +26 -3
- package/dist/isolation.js +222 -61
- package/dist/levels.d.ts +79 -0
- package/dist/levels.js +140 -0
- package/dist/observe.d.ts +9 -0
- package/dist/observe.js +27 -3
- package/dist/recall.d.ts +27 -1
- package/dist/recall.js +163 -17
- package/dist/store-conformance.js +69 -16
- package/package.json +1 -1
package/dist/grant.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
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';
|
|
2
8
|
/**
|
|
3
9
|
* The permission every `MemoryStore` method takes. Opaque: built only by
|
|
4
10
|
* `localGrant`, `principalGrant` or `nodeGrant`, frozen, and nominal, so a raw
|
|
@@ -21,10 +27,18 @@ export class Grant {
|
|
|
21
27
|
write;
|
|
22
28
|
readNodes;
|
|
23
29
|
writeNodes;
|
|
24
|
-
|
|
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) {
|
|
25
38
|
this.principal = Object.freeze({ ...principal });
|
|
26
39
|
this.readNodes = Object.freeze(readNodes.map(freezePattern));
|
|
27
40
|
this.writeNodes = Object.freeze(writeNodes.map(freezePattern));
|
|
41
|
+
this.home = Object.freeze(homeNode(principal, this.writeNodes, home));
|
|
28
42
|
this.read = Object.freeze({
|
|
29
43
|
...principal,
|
|
30
44
|
projectIds: legacyProjects(principal, this.readNodes),
|
|
@@ -36,14 +50,27 @@ export class Grant {
|
|
|
36
50
|
Object.freeze(this);
|
|
37
51
|
}
|
|
38
52
|
/** @internal Use `localGrant` or `principalGrant`. */
|
|
39
|
-
static issue(principal, read, write) {
|
|
53
|
+
static issue(principal, read, write, home = 'planet') {
|
|
40
54
|
checkPrincipal(principal);
|
|
41
|
-
|
|
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));
|
|
42
69
|
}
|
|
43
|
-
/** @internal Use `nodeGrant`. */
|
|
44
|
-
static issueNodes(principal, read, write) {
|
|
70
|
+
/** @internal Use `nodeGrant` or `narrowGrant`. */
|
|
71
|
+
static issueNodes(principal, read, write, home) {
|
|
45
72
|
checkPrincipal(principal);
|
|
46
|
-
return new Grant(principal, read, write);
|
|
73
|
+
return new Grant(principal, read, write, home);
|
|
47
74
|
}
|
|
48
75
|
/** True for a real grant (not a copy, not a look-alike). */
|
|
49
76
|
static is(x) {
|
|
@@ -57,13 +84,41 @@ function checkPrincipal(principal) {
|
|
|
57
84
|
}
|
|
58
85
|
}
|
|
59
86
|
}
|
|
60
|
-
/**
|
|
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
|
+
*/
|
|
61
92
|
function ownPlanet(p, projectIds) {
|
|
62
|
-
return {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
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;
|
|
67
122
|
}
|
|
68
123
|
function freezePattern(p) {
|
|
69
124
|
if (typeof p !== 'object' || p === null)
|
|
@@ -83,12 +138,13 @@ function freezePattern(p) {
|
|
|
83
138
|
}
|
|
84
139
|
/**
|
|
85
140
|
* The projects of `patterns` that a pre-level reader may assume: those of
|
|
86
|
-
* patterns naming exactly the principal's Planet
|
|
87
|
-
* Never wider than the
|
|
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.
|
|
88
144
|
*/
|
|
89
145
|
function legacyProjects(p, patterns) {
|
|
90
146
|
const own = nodeKey({ level: 'user', orgId: p.orgId, customerId: p.customerId });
|
|
91
|
-
const mine = patterns.filter((x) => x.authorAgentId === p.agentId && nodeKey(x.node) === own);
|
|
147
|
+
const mine = patterns.filter((x) => (x.authorAgentId === undefined || x.authorAgentId === p.agentId) && nodeKey(x.node) === own);
|
|
92
148
|
if (mine.some((x) => x.projectIds === 'all'))
|
|
93
149
|
return 'all';
|
|
94
150
|
return Object.freeze([
|
|
@@ -111,9 +167,17 @@ function samePrincipal(a, b) {
|
|
|
111
167
|
return a.orgId === b.orgId && a.customerId === b.customerId && a.agentId === b.agentId;
|
|
112
168
|
}
|
|
113
169
|
/**
|
|
114
|
-
* The grant for
|
|
115
|
-
*
|
|
116
|
-
* `write`
|
|
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.
|
|
117
181
|
*/
|
|
118
182
|
export function localGrant(read, opts = {}) {
|
|
119
183
|
const write = opts.write ?? read;
|
|
@@ -124,11 +188,15 @@ export function localGrant(read, opts = {}) {
|
|
|
124
188
|
if (!samePrincipal(read, write)) {
|
|
125
189
|
throw new RangeError('localGrant: read and write must name the same org, customer and agent');
|
|
126
190
|
}
|
|
127
|
-
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);
|
|
128
192
|
}
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
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);
|
|
132
200
|
}
|
|
133
201
|
/**
|
|
134
202
|
* A grant over explicit node patterns, possibly across owners and levels:
|
|
@@ -142,7 +210,37 @@ export function nodeGrant(opts) {
|
|
|
142
210
|
if (!Array.isArray(opts.read) || (opts.write !== undefined && !Array.isArray(opts.write))) {
|
|
143
211
|
throw new TypeError('nodeGrant: read and write must be arrays of node patterns');
|
|
144
212
|
}
|
|
145
|
-
return Grant.issueNodes(opts.principal, opts.read, opts.write ?? opts.read);
|
|
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()];
|
|
236
|
+
}
|
|
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
|
+
};
|
|
146
244
|
}
|
|
147
245
|
/** Rejects anything that is not a real `Grant`; every store calls it first. */
|
|
148
246
|
export function assertGrant(g) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.5.0";
|
|
2
2
|
export * from './model.js';
|
|
3
3
|
export * from './address.js';
|
|
4
4
|
export * from './export-format.js';
|
|
@@ -8,6 +8,8 @@ export * from './activation.js';
|
|
|
8
8
|
export * from './adapters.js';
|
|
9
9
|
export * from './embed-cache.js';
|
|
10
10
|
export * from './grant.js';
|
|
11
|
+
export * from './levels.js';
|
|
12
|
+
export * from './agents.js';
|
|
11
13
|
export * from './fusion.js';
|
|
12
14
|
export * from './memory-store.js';
|
|
13
15
|
export * from './tokenize.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.5.0';
|
|
2
2
|
export * from './model.js';
|
|
3
3
|
export * from './address.js';
|
|
4
4
|
export * from './export-format.js';
|
|
@@ -8,6 +8,8 @@ export * from './activation.js';
|
|
|
8
8
|
export * from './adapters.js';
|
|
9
9
|
export * from './embed-cache.js';
|
|
10
10
|
export * from './grant.js';
|
|
11
|
+
export * from './levels.js';
|
|
12
|
+
export * from './agents.js';
|
|
11
13
|
export * from './fusion.js';
|
|
12
14
|
export * from './memory-store.js';
|
|
13
15
|
export * from './tokenize.js';
|
package/dist/isolation.d.ts
CHANGED
|
@@ -10,8 +10,14 @@ import { type Grant } from './grant.js';
|
|
|
10
10
|
import type { Memory, Scope } from './model.js';
|
|
11
11
|
/** A memory for the isolation checks: text, stub and entities all carry `marker`. */
|
|
12
12
|
export declare function isoMemory(id: string, scope: Scope, marker: string, embedding?: Int8Array): Memory;
|
|
13
|
-
/**
|
|
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
|
+
*/
|
|
14
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;
|
|
15
21
|
/** The narrowest grant for one memory at any level: its node, its one project, its author. */
|
|
16
22
|
export declare function soleGrantFor(m: Pick<Memory, 'scope' | 'address' | 'author'>): Grant;
|
|
17
23
|
/** `m` moved to `scope`: its address and author dropped, so the scope's Planet is the address. */
|
|
@@ -59,13 +65,20 @@ export declare const ISO_SCOPES: {
|
|
|
59
65
|
/**
|
|
60
66
|
* Victims at the other levels, each next to the caller's own Planet: a Planet
|
|
61
67
|
* memory another of the caller's agents wrote, the caller's agent's own Star,
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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`.
|
|
64
72
|
*/
|
|
65
73
|
export declare const ISO_LEVELS: Record<string, {
|
|
66
74
|
address: Address;
|
|
67
75
|
author: Author;
|
|
68
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[];
|
|
69
82
|
/**
|
|
70
83
|
* Every `MemoryStore` method, called with a grant for `ISO_SCOPES.mine`
|
|
71
84
|
* against memories (warm and cold) in each other scope: nothing is seen,
|
|
@@ -73,6 +86,10 @@ export declare const ISO_LEVELS: Record<string, {
|
|
|
73
86
|
* apart. Also the write-side rules (`GrantError` for an out-of-grant scope
|
|
74
87
|
* and for an id that exists elsewhere, atomic batches, address changes),
|
|
75
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.
|
|
76
93
|
*/
|
|
77
94
|
export declare function isolationViolations(make: () => Promise<MemoryStore>): Promise<string[]>;
|
|
78
95
|
/** A small seeded PRNG (mulberry32), so a failing seed replays exactly. */
|
|
@@ -94,7 +111,13 @@ export declare function seededRng(seed: number): {
|
|
|
94
111
|
* `multiNode`: A's grant spans its Planet, its agent's Star and a
|
|
95
112
|
* Constellation of another owner; B's spans its own Planet and a sibling
|
|
96
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.
|
|
97
119
|
*/
|
|
98
120
|
export declare function nonInterference(make: () => Promise<MemoryStore>, seed: number, steps: number, opts?: {
|
|
99
121
|
multiNode?: boolean;
|
|
122
|
+
peers?: boolean;
|
|
100
123
|
}): Promise<string | null>;
|
package/dist/isolation.js
CHANGED
|
@@ -34,8 +34,25 @@ export function isoMemory(id, scope, marker, embedding) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
const principal = (s) => ({ orgId: s.orgId, customerId: s.customerId, agentId: s.agentId });
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The narrowest grant for one scope: its Planet, its one project (or global),
|
|
39
|
+
* memories its own agent wrote. (The pre-CON-62 local grant; today's
|
|
40
|
+
* `localGrant` also covers the Planet's other authors and the agent's Star.)
|
|
41
|
+
*/
|
|
38
42
|
export function soleGrant(s) {
|
|
43
|
+
return nodeGrant({
|
|
44
|
+
principal: principal(s),
|
|
45
|
+
read: [
|
|
46
|
+
{
|
|
47
|
+
node: { level: 'user', orgId: s.orgId, customerId: s.customerId },
|
|
48
|
+
projectIds: [s.projectId ?? null],
|
|
49
|
+
authorAgentId: s.agentId,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The local grant of `ISO_SCOPES.mine`'s agent for its one project: Planet plus its own Star. */
|
|
55
|
+
export function defaultGrant(s) {
|
|
39
56
|
return localGrant({ ...principal(s), projectIds: [s.projectId ?? null] });
|
|
40
57
|
}
|
|
41
58
|
/** The narrowest grant for one memory at any level: its node, its one project, its author. */
|
|
@@ -92,8 +109,10 @@ const ISO_PRINCIPAL = { orgId: 'local', customerId: 'self', agentId: 'claude-cod
|
|
|
92
109
|
/**
|
|
93
110
|
* Victims at the other levels, each next to the caller's own Planet: a Planet
|
|
94
111
|
* memory another of the caller's agents wrote, the caller's agent's own Star,
|
|
95
|
-
*
|
|
96
|
-
*
|
|
112
|
+
* another agent's Star (in the caller's project and global), an Orbit, a
|
|
113
|
+
* Flyby, a Constellation and a Galaxy root of an organisation, and one of the
|
|
114
|
+
* caller's own owner. The narrowest grant sees none of them; the local grant
|
|
115
|
+
* sees only those in `DEFAULT_COVERS`.
|
|
97
116
|
*/
|
|
98
117
|
export const ISO_LEVELS = {
|
|
99
118
|
planetOtherAuthor: {
|
|
@@ -104,6 +123,14 @@ export const ISO_LEVELS = {
|
|
|
104
123
|
address: { level: 'agent', orgId: 'local', agentId: 'claude-code', projectId: 'pa' },
|
|
105
124
|
author: { agentId: 'claude-code' },
|
|
106
125
|
},
|
|
126
|
+
otherStar: {
|
|
127
|
+
address: { level: 'agent', orgId: 'local', agentId: 'cursor', projectId: 'pa' },
|
|
128
|
+
author: { agentId: 'cursor' },
|
|
129
|
+
},
|
|
130
|
+
otherStarGlobal: {
|
|
131
|
+
address: { level: 'agent', orgId: 'local', agentId: 'cursor' },
|
|
132
|
+
author: { agentId: 'cursor' },
|
|
133
|
+
},
|
|
107
134
|
orbit: {
|
|
108
135
|
address: { level: 'agent-user', orgId: 'local', agentId: 'claude-code', customerId: 'self' },
|
|
109
136
|
author: { agentId: 'claude-code' },
|
|
@@ -132,6 +159,15 @@ export const ISO_LEVELS = {
|
|
|
132
159
|
author: { agentId: 'claude-code' },
|
|
133
160
|
},
|
|
134
161
|
};
|
|
162
|
+
/**
|
|
163
|
+
* The victims the local grant of `ISO_SCOPES.mine` legitimately covers (CON-62): Planet memories
|
|
164
|
+
* the person's other agents wrote, and its agent's own Star. Never another agent's Star.
|
|
165
|
+
*/
|
|
166
|
+
export const DEFAULT_COVERS = Object.freeze([
|
|
167
|
+
'otherAgent',
|
|
168
|
+
'planetOtherAuthor',
|
|
169
|
+
'star',
|
|
170
|
+
]);
|
|
135
171
|
/**
|
|
136
172
|
* Every `MemoryStore` method, called with a grant for `ISO_SCOPES.mine`
|
|
137
173
|
* against memories (warm and cold) in each other scope: nothing is seen,
|
|
@@ -139,6 +175,10 @@ export const ISO_LEVELS = {
|
|
|
139
175
|
* apart. Also the write-side rules (`GrantError` for an out-of-grant scope
|
|
140
176
|
* and for an id that exists elsewhere, atomic batches, address changes),
|
|
141
177
|
* read/write splits, cursor namespaces and rejection of non-grants.
|
|
178
|
+
*
|
|
179
|
+
* Run twice: as the narrowest grant (every victim out of reach), and as the
|
|
180
|
+
* local grant (`localGrant`, Planet plus own Star), for which every victim
|
|
181
|
+
* but `DEFAULT_COVERS` is out of reach, other agents' Stars included.
|
|
142
182
|
*/
|
|
143
183
|
export async function isolationViolations(make) {
|
|
144
184
|
const out = [];
|
|
@@ -176,62 +216,12 @@ export async function isolationViolations(make) {
|
|
|
176
216
|
}
|
|
177
217
|
}
|
|
178
218
|
const snapshot = async () => Promise.all(victims.map(async (v) => `${render(await store.get(v.g, v.m.id))}|${render(await store.coldGet(v.g, v.m.id))}`));
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if ((await store.coldGet(mine, id)) !== null)
|
|
186
|
-
fail('coldGet returned it');
|
|
187
|
-
const lex = await store.lexical(mine, 'shared canary', 50);
|
|
188
|
-
if (lex.some((h) => h.id === id))
|
|
189
|
-
fail('lexical returned it');
|
|
190
|
-
const den = await store.dense(mine, v.m.embedding ?? vec(1), 50);
|
|
191
|
-
if (den.some((h) => h.id === id))
|
|
192
|
-
fail('dense returned it');
|
|
193
|
-
for (const validOnly of [true, false]) {
|
|
194
|
-
if ((await store.list(mine, { validOnly })).some((m) => m.id === id))
|
|
195
|
-
fail('list returned it');
|
|
196
|
-
}
|
|
197
|
-
await store.recordAccess(mine, [id], new Date(NOW));
|
|
198
|
-
if ((await store.setTier(mine, id, 'hot')) !== false)
|
|
199
|
-
fail('setTier reported a write');
|
|
200
|
-
if ((await store.setTier(mine, id, 'hot', v.m)) !== false)
|
|
201
|
-
fail('guarded setTier reported a write');
|
|
202
|
-
const exp = await store.expireMany(mine, [v.m], new Date(NOW));
|
|
203
|
-
if (!exp.skipped.includes(id))
|
|
204
|
-
fail('expireMany did not skip it');
|
|
205
|
-
for (const r of [await store.coldPut(mine, v.m), await store.coldPutMany(mine, [v.m])]) {
|
|
206
|
-
if (!r.skipped.includes(id))
|
|
207
|
-
fail('coldPut did not skip it');
|
|
208
|
-
}
|
|
209
|
-
await store.forget(mine, id);
|
|
210
|
-
const direct = await settle(store.upsert(mine, v.m));
|
|
211
|
-
if (!('err' in direct) || !(direct.err instanceof GrantError)) {
|
|
212
|
-
fail('upsert of its own scope was not refused with GrantError');
|
|
213
|
-
}
|
|
214
|
-
const moved = await settle(store.upsert(mine, movedTo(v.m, ISO_SCOPES.mine)));
|
|
215
|
-
if (!('err' in moved) || !(moved.err instanceof GrantError)) {
|
|
216
|
-
fail('upsert moving its id into my scope was not refused with GrantError');
|
|
217
|
-
}
|
|
218
|
-
else if ('err' in direct &&
|
|
219
|
-
direct.err.message !== moved.err.message) {
|
|
220
|
-
fail('the two refusals have different messages');
|
|
221
|
-
}
|
|
222
|
-
const batch = await settle(store.upsertMany(mine, [
|
|
223
|
-
isoMemory(`batch-${id}`, ISO_SCOPES.mine, 'batch'),
|
|
224
|
-
movedTo(v.m, ISO_SCOPES.mine),
|
|
225
|
-
]));
|
|
226
|
-
if (!('err' in batch))
|
|
227
|
-
fail('a batch reaching it was not refused');
|
|
228
|
-
if ((await store.get(mine, `batch-${id}`)) !== null)
|
|
229
|
-
fail('a refused batch wrote part of itself');
|
|
230
|
-
}
|
|
231
|
-
const after = await snapshot();
|
|
232
|
-
for (const [i, v] of victims.entries()) {
|
|
233
|
-
if (before[i] !== after[i])
|
|
234
|
-
out.push(`${v.name}: changed through a grant that cannot see it`);
|
|
219
|
+
for (const [caller, grant, covered] of [
|
|
220
|
+
['narrowest', mine, []],
|
|
221
|
+
['local', defaultGrant(ISO_SCOPES.mine), DEFAULT_COVERS],
|
|
222
|
+
]) {
|
|
223
|
+
const reach = victims.filter((v) => !covered.some((c) => v.name === `victim-${c}-warm` || v.name === `victim-${c}-cold`));
|
|
224
|
+
out.push(...(await victimViolations(store, caller, grant, reach, snapshot, victims)));
|
|
235
225
|
}
|
|
236
226
|
// An address change of my own record is refused too, and leaves it as it was.
|
|
237
227
|
const own = await store.get(mine, 'mine-1');
|
|
@@ -312,6 +302,68 @@ export async function isolationViolations(make) {
|
|
|
312
302
|
}
|
|
313
303
|
return out;
|
|
314
304
|
}
|
|
305
|
+
/** One caller's pass over the victims it must not reach: nothing seen, nothing changed. */
|
|
306
|
+
async function victimViolations(store, caller, mine, victims, snapshot, all) {
|
|
307
|
+
const out = [];
|
|
308
|
+
const vec = (n) => Int8Array.from([n, 1, 1, 1]);
|
|
309
|
+
const before = await snapshot();
|
|
310
|
+
for (const v of victims) {
|
|
311
|
+
const id = v.m.id;
|
|
312
|
+
const fail = (what) => out.push(`${caller}: ${v.name}: ${what}`);
|
|
313
|
+
if ((await store.get(mine, id)) !== null)
|
|
314
|
+
fail('get returned it');
|
|
315
|
+
if ((await store.coldGet(mine, id)) !== null)
|
|
316
|
+
fail('coldGet returned it');
|
|
317
|
+
const lex = await store.lexical(mine, 'shared canary', 50);
|
|
318
|
+
if (lex.some((h) => h.id === id))
|
|
319
|
+
fail('lexical returned it');
|
|
320
|
+
const den = await store.dense(mine, v.m.embedding ?? vec(1), 50);
|
|
321
|
+
if (den.some((h) => h.id === id))
|
|
322
|
+
fail('dense returned it');
|
|
323
|
+
for (const validOnly of [true, false]) {
|
|
324
|
+
if ((await store.list(mine, { validOnly })).some((m) => m.id === id))
|
|
325
|
+
fail('list returned it');
|
|
326
|
+
}
|
|
327
|
+
await store.recordAccess(mine, [id], new Date(NOW));
|
|
328
|
+
if ((await store.setTier(mine, id, 'hot')) !== false)
|
|
329
|
+
fail('setTier reported a write');
|
|
330
|
+
if ((await store.setTier(mine, id, 'hot', v.m)) !== false)
|
|
331
|
+
fail('guarded setTier reported a write');
|
|
332
|
+
const exp = await store.expireMany(mine, [v.m], new Date(NOW));
|
|
333
|
+
if (!exp.skipped.includes(id))
|
|
334
|
+
fail('expireMany did not skip it');
|
|
335
|
+
for (const r of [await store.coldPut(mine, v.m), await store.coldPutMany(mine, [v.m])]) {
|
|
336
|
+
if (!r.skipped.includes(id))
|
|
337
|
+
fail('coldPut did not skip it');
|
|
338
|
+
}
|
|
339
|
+
await store.forget(mine, id);
|
|
340
|
+
const direct = await settle(store.upsert(mine, v.m));
|
|
341
|
+
if (!('err' in direct) || !(direct.err instanceof GrantError)) {
|
|
342
|
+
fail('upsert of its own scope was not refused with GrantError');
|
|
343
|
+
}
|
|
344
|
+
const moved = await settle(store.upsert(mine, movedTo(v.m, ISO_SCOPES.mine)));
|
|
345
|
+
if (!('err' in moved) || !(moved.err instanceof GrantError)) {
|
|
346
|
+
fail('upsert moving its id into my scope was not refused with GrantError');
|
|
347
|
+
}
|
|
348
|
+
else if ('err' in direct && direct.err.message !== moved.err.message) {
|
|
349
|
+
fail('the two refusals have different messages');
|
|
350
|
+
}
|
|
351
|
+
const batch = await settle(store.upsertMany(mine, [
|
|
352
|
+
isoMemory(`batch-${id}`, ISO_SCOPES.mine, 'batch'),
|
|
353
|
+
movedTo(v.m, ISO_SCOPES.mine),
|
|
354
|
+
]));
|
|
355
|
+
if (!('err' in batch))
|
|
356
|
+
fail('a batch reaching it was not refused');
|
|
357
|
+
if ((await store.get(mine, `batch-${id}`)) !== null)
|
|
358
|
+
fail('a refused batch wrote part of itself');
|
|
359
|
+
}
|
|
360
|
+
const after = await snapshot();
|
|
361
|
+
for (const [i, v] of all.entries()) {
|
|
362
|
+
if (before[i] !== after[i])
|
|
363
|
+
out.push(`${caller}: ${v.name}: changed through a grant that cannot see it`);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
315
367
|
/** A small seeded PRNG (mulberry32), so a failing seed replays exactly. */
|
|
316
368
|
export function seededRng(seed) {
|
|
317
369
|
let a = seed >>> 0;
|
|
@@ -336,7 +388,10 @@ const WORDS = ['orbit', 'comet', 'nebula', 'pulsar', 'quasar', 'photon', 'kuiper
|
|
|
336
388
|
* keys (callers use UUIDs), so a tenant that reuses another tenant's id is
|
|
337
389
|
* refused, which is a collision, not a leak of content.
|
|
338
390
|
*/
|
|
339
|
-
function opSequence(seed, steps,
|
|
391
|
+
function opSequence(seed, steps, mode) {
|
|
392
|
+
if (mode === 'peers')
|
|
393
|
+
return peerSequence(seed, steps);
|
|
394
|
+
const multiNode = mode === 'multi';
|
|
340
395
|
const rng = seededRng(seed);
|
|
341
396
|
const planetA = ['pa', 'pa2', null].map((projectId) => (id, t, e) => isoMemory(id, projectId === null ? ISO_SCOPES.global : { ...ISO_SCOPES.mine, projectId }, t, e));
|
|
342
397
|
// With multiNode, A also reads and writes a Constellation of another owner and its
|
|
@@ -453,6 +508,107 @@ function opSequence(seed, steps, multiNode) {
|
|
|
453
508
|
}
|
|
454
509
|
return ops;
|
|
455
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Two agents of one person (see `nonInterference`'s `peers`): A is `claude-code`, B is `cursor`,
|
|
513
|
+
* each with its local grant over projects pa, pa2 and global.
|
|
514
|
+
*/
|
|
515
|
+
function peerSequence(seed, steps) {
|
|
516
|
+
const rng = seededRng(seed);
|
|
517
|
+
const person = { orgId: 'local', customerId: 'self' };
|
|
518
|
+
const projects = ['pa', 'pa2', null];
|
|
519
|
+
const aGrant = localGrant({ ...person, agentId: 'claude-code', projectIds: [...projects] });
|
|
520
|
+
const bGrant = localGrant({ ...person, agentId: 'cursor', projectIds: [...projects] });
|
|
521
|
+
const star = (agentId, projectId) => ({
|
|
522
|
+
level: 'agent',
|
|
523
|
+
orgId: 'local',
|
|
524
|
+
agentId,
|
|
525
|
+
...(projectId !== null && { projectId }),
|
|
526
|
+
});
|
|
527
|
+
const ops = [];
|
|
528
|
+
const aIds = [];
|
|
529
|
+
const aStarIds = [];
|
|
530
|
+
const bIds = [];
|
|
531
|
+
const text = () => `${rng.pick(WORDS)} ${rng.pick(WORDS)} ${rng.pick(WORDS)}`;
|
|
532
|
+
const emb = () => Int8Array.from([rng.int(9) - 4, rng.int(9) - 4, rng.int(9) - 4, 1]);
|
|
533
|
+
for (let i = 0; i < steps; i++) {
|
|
534
|
+
const who = rng.int(2) === 0 ? 'A' : 'B';
|
|
535
|
+
const grant = who === 'A' ? aGrant : bGrant;
|
|
536
|
+
const own = who === 'A' ? aIds : bIds;
|
|
537
|
+
// B aims at A's Star ids half the time (A's Planet is B's to share); A only at its own.
|
|
538
|
+
const target = () => who === 'B' && aStarIds.length > 0 && rng.int(2) === 0
|
|
539
|
+
? rng.pick(aStarIds)
|
|
540
|
+
: own.length > 0
|
|
541
|
+
? rng.pick(own)
|
|
542
|
+
: 'none';
|
|
543
|
+
const kind = rng.int(12);
|
|
544
|
+
if (kind <= 2 || own.length === 0) {
|
|
545
|
+
const id = `${who}-${i}`;
|
|
546
|
+
own.push(id);
|
|
547
|
+
const projectId = rng.pick(projects);
|
|
548
|
+
let m;
|
|
549
|
+
if (who === 'B') {
|
|
550
|
+
m = isoAt(id, star('cursor', projectId), { agentId: 'cursor' }, text(), emb());
|
|
551
|
+
}
|
|
552
|
+
else if (rng.int(2) === 0) {
|
|
553
|
+
aStarIds.push(id);
|
|
554
|
+
m = isoAt(id, star('claude-code', projectId), { agentId: 'claude-code' }, text(), emb());
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
const scope = {
|
|
558
|
+
...person,
|
|
559
|
+
agentId: 'claude-code',
|
|
560
|
+
...(projectId !== null && { projectId }),
|
|
561
|
+
};
|
|
562
|
+
m = isoMemory(id, scope, text(), emb());
|
|
563
|
+
}
|
|
564
|
+
const salted = { ...m, salience: rng.int(10) / 10 };
|
|
565
|
+
ops.push({ who, run: (s) => s.upsert(grant, salted) });
|
|
566
|
+
}
|
|
567
|
+
else if (kind === 3) {
|
|
568
|
+
const id = target();
|
|
569
|
+
ops.push({ who, run: async (s) => render(await s.get(grant, id)) });
|
|
570
|
+
}
|
|
571
|
+
else if (kind === 4) {
|
|
572
|
+
const q = text();
|
|
573
|
+
ops.push({ who, run: async (s) => (await s.lexical(grant, q, 100)).map((h) => h.id).sort() });
|
|
574
|
+
}
|
|
575
|
+
else if (kind === 5) {
|
|
576
|
+
const v = emb();
|
|
577
|
+
ops.push({ who, run: async (s) => s.dense(grant, v, 5) });
|
|
578
|
+
}
|
|
579
|
+
else if (kind === 6) {
|
|
580
|
+
ops.push({ who, run: async (s) => (await s.list(grant, { validOnly: false })).map(render) });
|
|
581
|
+
}
|
|
582
|
+
else if (kind === 7) {
|
|
583
|
+
const id = target();
|
|
584
|
+
ops.push({
|
|
585
|
+
who,
|
|
586
|
+
run: async (s) => {
|
|
587
|
+
const m = await s.get(grant, id);
|
|
588
|
+
return m === null ? 'none' : s.coldPut(grant, m);
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
else if (kind === 8) {
|
|
593
|
+
const id = target();
|
|
594
|
+
ops.push({ who, run: async (s) => render(await s.coldGet(grant, id)) });
|
|
595
|
+
}
|
|
596
|
+
else if (kind === 9) {
|
|
597
|
+
const id = target();
|
|
598
|
+
const tier = rng.pick(['hot', 'warm']);
|
|
599
|
+
ops.push({ who, run: (s) => s.setTier(grant, id, tier) });
|
|
600
|
+
}
|
|
601
|
+
else if (kind === 10) {
|
|
602
|
+
const ids = [target(), target()];
|
|
603
|
+
ops.push({ who, run: (s) => s.recordAccess(grant, ids, new Date(NOW)) });
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
const id = target();
|
|
607
|
+
ops.push({ who, run: (s) => s.forget(grant, id) });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return ops;
|
|
611
|
+
}
|
|
456
612
|
/**
|
|
457
613
|
* Non-interference: tenant A's observable results (return values, errors,
|
|
458
614
|
* order and scores) are the same whether or not tenant B ran its operations
|
|
@@ -467,9 +623,14 @@ function opSequence(seed, steps, multiNode) {
|
|
|
467
623
|
* `multiNode`: A's grant spans its Planet, its agent's Star and a
|
|
468
624
|
* Constellation of another owner; B's spans its own Planet and a sibling
|
|
469
625
|
* Constellation. Same property.
|
|
626
|
+
*
|
|
627
|
+
* `peers` (CON-62): A and B are two agents of one person, each with the local
|
|
628
|
+
* grant (the shared Planet plus its own Star). A writes to the Planet and its
|
|
629
|
+
* Star; B writes only to its own Star and aims every method at A's Star ids.
|
|
630
|
+
* Nothing B does through its Star reaches A.
|
|
470
631
|
*/
|
|
471
632
|
export async function nonInterference(make, seed, steps, opts = {}) {
|
|
472
|
-
const ops = opSequence(seed, steps, opts.multiNode === true);
|
|
633
|
+
const ops = opSequence(seed, steps, opts.peers === true ? 'peers' : opts.multiNode === true ? 'multi' : 'single');
|
|
473
634
|
const observe = async (withB) => {
|
|
474
635
|
const s = await make();
|
|
475
636
|
const seen = [];
|