@oortmem/core 0.2.0 → 0.3.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/address.d.ts +161 -0
- package/dist/address.js +225 -0
- package/dist/date-cue.d.ts +26 -0
- package/dist/date-cue.js +70 -0
- package/dist/export-format.d.ts +77 -0
- package/dist/export-format.js +168 -0
- package/dist/grant.d.ts +69 -9
- package/dist/grant.js +126 -17
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/isolation.d.ts +24 -1
- package/dist/isolation.js +146 -16
- package/dist/memory-store.js +27 -23
- package/dist/model.d.ts +17 -0
- package/dist/store-conformance.d.ts +12 -0
- package/dist/store-conformance.js +312 -1
- package/dist/temporal.js +12 -7
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Oort export format (docs/export-format.md): JSON Lines, a manifest line,
|
|
3
|
+
* then one memory per line. Version 2 (written by default) adds `address`,
|
|
4
|
+
* `author` and `provenance` to every line and keeps `scope` on Planet
|
|
5
|
+
* memories; version 1 has `scope` only and can hold Planet memories only.
|
|
6
|
+
* Readers accept both, version 1 forever.
|
|
7
|
+
*/
|
|
8
|
+
import { addressOf, authorOf, canonicalMemory, planetOf, resolveAddressing, scopeOf, } from './address.js';
|
|
9
|
+
/** The version `oort export` writes. */
|
|
10
|
+
export const EXPORT_FORMAT_VERSION = 2;
|
|
11
|
+
/** Raised for a memory a format version cannot hold, or a file that is not a readable export. */
|
|
12
|
+
export class ExportFormatError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'ExportFormatError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const base64 = (v) => Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('base64');
|
|
19
|
+
/** True when version 1 can hold `m`: a Planet memory whose author is its scope's agent. */
|
|
20
|
+
export function fitsVersion1(m) {
|
|
21
|
+
const author = authorOf(m);
|
|
22
|
+
return (addressOf(m).level === 'user' &&
|
|
23
|
+
author.accountRef === undefined &&
|
|
24
|
+
author.agentId === m.scope.agentId &&
|
|
25
|
+
(m.provenance === undefined || m.provenance === null));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One memory as an export line of `version`: the embedding as base64 of its
|
|
29
|
+
* int8 bytes. Version 2 always writes `address`, `author` and `provenance`
|
|
30
|
+
* (null when none), and `scope` for Planet memories only.
|
|
31
|
+
*/
|
|
32
|
+
export function exportLine(m, version) {
|
|
33
|
+
const { embedding, address: _a, author: _w, provenance, scope, id, ...rest } = m;
|
|
34
|
+
const tail = embedding === undefined ? {} : { embedding: base64(embedding) };
|
|
35
|
+
if (version === 1) {
|
|
36
|
+
if (!fitsVersion1(m)) {
|
|
37
|
+
throw new ExportFormatError(`memory ${id} is not a Planet memory; version 1 cannot hold it`);
|
|
38
|
+
}
|
|
39
|
+
return { id, scope, ...rest, ...tail };
|
|
40
|
+
}
|
|
41
|
+
const address = addressOf(m);
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
...(address.level === 'user' && { scope }),
|
|
45
|
+
address,
|
|
46
|
+
author: authorOf(m),
|
|
47
|
+
provenance: provenance ?? null,
|
|
48
|
+
...rest,
|
|
49
|
+
...tail,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const keyOf = (x) => JSON.stringify(Object.entries(x).sort());
|
|
53
|
+
/** The manifest for `memories` written as `version`. */
|
|
54
|
+
export function exportManifest(memories, version, exportedAt = new Date()) {
|
|
55
|
+
const scopes = new Map();
|
|
56
|
+
const addresses = new Map();
|
|
57
|
+
for (const m of memories) {
|
|
58
|
+
const a = addressOf(m);
|
|
59
|
+
if (a.level === 'user')
|
|
60
|
+
scopes.set(keyOf(m.scope), m.scope);
|
|
61
|
+
addresses.set(keyOf(a), a);
|
|
62
|
+
}
|
|
63
|
+
const base = {
|
|
64
|
+
oort: 'export',
|
|
65
|
+
exportedAt: exportedAt.toISOString(),
|
|
66
|
+
memories: memories.length,
|
|
67
|
+
scopes: [...scopes.values()],
|
|
68
|
+
};
|
|
69
|
+
return version === 1
|
|
70
|
+
? { ...base, version: 1 }
|
|
71
|
+
: { ...base, version: 2, addresses: [...addresses.values()] };
|
|
72
|
+
}
|
|
73
|
+
/** A whole export: the manifest line, then one line per memory, each ending in `\n`. */
|
|
74
|
+
export function formatExport(memories, version = EXPORT_FORMAT_VERSION, exportedAt = new Date()) {
|
|
75
|
+
return [
|
|
76
|
+
exportManifest(memories, version, exportedAt),
|
|
77
|
+
...memories.map((m) => exportLine(m, version)),
|
|
78
|
+
]
|
|
79
|
+
.map((x) => `${JSON.stringify(x)}\n`)
|
|
80
|
+
.join('');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Reads an export of version 1 or 2. A version 1 line's `scope` names the
|
|
84
|
+
* memory's Planet and its agent the author. Throws `ExportFormatError` for an
|
|
85
|
+
* unknown version, a count that does not match, or a line that is not a
|
|
86
|
+
* memory (with its line number, never its content).
|
|
87
|
+
*/
|
|
88
|
+
export function parseExport(text, opts = {}) {
|
|
89
|
+
const lines = text.split('\n').filter((l) => l.trim() !== '');
|
|
90
|
+
const head = parseJson(lines[0], 1);
|
|
91
|
+
if (head?.oort !== 'export')
|
|
92
|
+
throw new ExportFormatError('line 1 is not an Oort export manifest');
|
|
93
|
+
if (head.version !== 1 && head.version !== 2) {
|
|
94
|
+
throw new ExportFormatError(`export version ${String(head.version)} is not supported`);
|
|
95
|
+
}
|
|
96
|
+
const version = head.version;
|
|
97
|
+
if (head.memories !== lines.length - 1) {
|
|
98
|
+
throw new ExportFormatError(`the manifest counts ${String(head.memories)} memories, the file has ${lines.length - 1}`);
|
|
99
|
+
}
|
|
100
|
+
const memories = lines.slice(1).map((l, i) => {
|
|
101
|
+
const n = i + 2;
|
|
102
|
+
const raw = parseJson(l, n);
|
|
103
|
+
if (typeof raw !== 'object' || raw === null || typeof raw['id'] !== 'string') {
|
|
104
|
+
throw new ExportFormatError(`line ${n} is not a memory`);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return readLine(raw, version, opts);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (e instanceof RangeError || e instanceof TypeError) {
|
|
111
|
+
throw new ExportFormatError(`line ${n}: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
throw e;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return { version, manifest: head, memories };
|
|
117
|
+
}
|
|
118
|
+
function parseJson(line, n) {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(line ?? '');
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new ExportFormatError(`line ${n} is not JSON`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function readLine(raw, version, opts) {
|
|
127
|
+
const { embedding, address: rawAddress, author: rawAuthor, provenance, scope: rawScope, supersedesHidden: _sh, supersededByHidden: _bh, ...rest } = raw;
|
|
128
|
+
let address;
|
|
129
|
+
let author;
|
|
130
|
+
if (version === 1) {
|
|
131
|
+
const scope = rawScope;
|
|
132
|
+
if (typeof scope !== 'object' || scope === null)
|
|
133
|
+
throw new RangeError('a version 1 line needs scope');
|
|
134
|
+
address = planetOf(scope);
|
|
135
|
+
author = { agentId: scope.agentId };
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
({ address, author } = checked(rawAddress, rawAuthor, rawScope));
|
|
139
|
+
}
|
|
140
|
+
if (opts.planet !== undefined && address.level === 'user') {
|
|
141
|
+
address = { ...address, orgId: opts.planet.orgId, customerId: opts.planet.customerId };
|
|
142
|
+
}
|
|
143
|
+
const m = {
|
|
144
|
+
...rest,
|
|
145
|
+
scope: scopeOf(address, author),
|
|
146
|
+
address,
|
|
147
|
+
author,
|
|
148
|
+
...(provenance !== undefined && provenance !== null && { provenance }),
|
|
149
|
+
...(typeof embedding === 'string' && {
|
|
150
|
+
embedding: Int8Array.from(Buffer.from(embedding, 'base64')),
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
return canonicalMemory(m);
|
|
154
|
+
}
|
|
155
|
+
/** A version 2 line's address and author, checked, with its scope (when present) matching them. */
|
|
156
|
+
function checked(rawAddress, rawAuthor, rawScope) {
|
|
157
|
+
if (rawAddress === undefined || rawAuthor === undefined) {
|
|
158
|
+
throw new RangeError('a version 2 line needs address and author');
|
|
159
|
+
}
|
|
160
|
+
const probe = { address: rawAddress, author: rawAuthor };
|
|
161
|
+
const want = scopeOf(probe.address, probe.author);
|
|
162
|
+
const { address, author } = resolveAddressing({
|
|
163
|
+
...probe,
|
|
164
|
+
scope: rawScope === undefined ? want : rawScope,
|
|
165
|
+
});
|
|
166
|
+
return { address, author };
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=export-format.js.map
|
package/dist/grant.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { type Address, type Author, type NodeRef } from './address.js';
|
|
1
2
|
import type { ScopeFilter } from './adapters.js';
|
|
2
|
-
import type { Scope } from './model.js';
|
|
3
|
-
/**
|
|
3
|
+
import type { Memory, Scope } from './model.js';
|
|
4
|
+
/**
|
|
5
|
+
* Who a grant acts for: an account (`orgId`, `customerId`) and the agent
|
|
6
|
+
* acting for it. Cursors belong to the principal.
|
|
7
|
+
*/
|
|
4
8
|
export interface Principal {
|
|
5
9
|
orgId: string;
|
|
6
10
|
customerId: string;
|
|
@@ -11,30 +15,54 @@ export interface Principal {
|
|
|
11
15
|
* no project) or `'all'`, every project of the principal plus global.
|
|
12
16
|
*/
|
|
13
17
|
export type GrantProjects = readonly (string | null)[] | 'all';
|
|
14
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* One side (read or write) of a pre-level grant: the principal's Planet,
|
|
20
|
+
* written by the principal's agent, in `projectIds`.
|
|
21
|
+
* @deprecated Stores read `Grant.readNodes` and `Grant.writeNodes`.
|
|
22
|
+
*/
|
|
15
23
|
export interface GrantFilter extends Readonly<Principal> {
|
|
16
24
|
readonly projectIds: GrantProjects;
|
|
17
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* One node a grant covers (CON-61): the node, the project bindings inside it,
|
|
28
|
+
* and optionally one author. Matching is structural and exact on every field
|
|
29
|
+
* (never a prefix). `authorAgentId` restricts the pattern to memories that
|
|
30
|
+
* agent wrote; the pre-level constructors (`localGrant`, `principalGrant`)
|
|
31
|
+
* set it to the principal's agent, so they see exactly what they saw before
|
|
32
|
+
* levels existed.
|
|
33
|
+
*/
|
|
34
|
+
export interface NodePattern {
|
|
35
|
+
readonly node: NodeRef;
|
|
36
|
+
readonly projectIds: GrantProjects;
|
|
37
|
+
readonly authorAgentId?: string;
|
|
38
|
+
}
|
|
18
39
|
/**
|
|
19
40
|
* The permission every `MemoryStore` method takes. Opaque: built only by
|
|
20
|
-
* `localGrant` or `
|
|
41
|
+
* `localGrant`, `principalGrant` or `nodeGrant`, frozen, and nominal, so a raw
|
|
21
42
|
* `ScopeFilter` (or any look-alike object) is rejected by the type checker
|
|
22
43
|
* and, for JavaScript callers, by `assertGrant` inside every store.
|
|
23
44
|
*
|
|
24
|
-
* `
|
|
25
|
-
* `coldGet`, `recordAccess`); `
|
|
26
|
-
* `forget`, `setTier`, `expireMany`, `coldPut`).
|
|
27
|
-
*
|
|
45
|
+
* `readNodes` bounds what can be seen (`get`, `lexical`, `dense`, `list`,
|
|
46
|
+
* `coldGet`, `recordAccess`); `writeNodes` bounds what can change (`upsert`,
|
|
47
|
+
* `forget`, `setTier`, `expireMany`, `coldPut`). A memory is covered when
|
|
48
|
+
* any pattern of the side covers its address and author. Cursors belong to
|
|
49
|
+
* the principal. Anything outside a grant behaves exactly like something that
|
|
28
50
|
* does not exist.
|
|
29
51
|
*/
|
|
30
52
|
export declare class Grant {
|
|
31
53
|
#private;
|
|
32
54
|
readonly principal: Readonly<Principal>;
|
|
55
|
+
/** @deprecated The pre-level view of `readNodes`; see `GrantFilter`. */
|
|
33
56
|
readonly read: GrantFilter;
|
|
57
|
+
/** @deprecated The pre-level view of `writeNodes`; see `GrantFilter`. */
|
|
34
58
|
readonly write: GrantFilter;
|
|
59
|
+
readonly readNodes: readonly NodePattern[];
|
|
60
|
+
readonly writeNodes: readonly NodePattern[];
|
|
35
61
|
private constructor();
|
|
36
62
|
/** @internal Use `localGrant` or `principalGrant`. */
|
|
37
63
|
static issue(principal: Principal, read: GrantProjects, write: GrantProjects): Grant;
|
|
64
|
+
/** @internal Use `nodeGrant`. */
|
|
65
|
+
static issueNodes(principal: Principal, read: readonly NodePattern[], write: readonly NodePattern[]): Grant;
|
|
38
66
|
/** True for a real grant (not a copy, not a look-alike). */
|
|
39
67
|
static is(x: unknown): x is Grant;
|
|
40
68
|
}
|
|
@@ -48,12 +76,44 @@ export declare function localGrant(read: ScopeFilter, opts?: {
|
|
|
48
76
|
}): Grant;
|
|
49
77
|
/** Every project of `p` (and its global memories), for read and write: maintenance such as export. */
|
|
50
78
|
export declare function principalGrant(p: Principal): Grant;
|
|
79
|
+
/**
|
|
80
|
+
* A grant over explicit node patterns, possibly across owners and levels:
|
|
81
|
+
* `read` bounds what is seen, `write` (default: `read`) what may change.
|
|
82
|
+
* Trusted: whoever builds one has already decided the principal may use
|
|
83
|
+
* every node it names (local configuration, or membership checked by a
|
|
84
|
+
* control plane). Nothing in the engine builds one for its own callers yet:
|
|
85
|
+
* the CLI, hooks and MCP server keep the pre-level grants.
|
|
86
|
+
*/
|
|
87
|
+
export declare function nodeGrant(opts: {
|
|
88
|
+
principal: Principal;
|
|
89
|
+
read: readonly NodePattern[];
|
|
90
|
+
write?: readonly NodePattern[];
|
|
91
|
+
}): Grant;
|
|
51
92
|
/** Rejects anything that is not a real `Grant`; every store calls it first. */
|
|
52
93
|
export declare function assertGrant(g: unknown): asserts g is Grant;
|
|
53
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* True iff a memory with `scope` lies inside `f`. Extra scope fields do not widen or narrow it.
|
|
96
|
+
* @deprecated Use `grantReads` / `grantWrites`.
|
|
97
|
+
*/
|
|
54
98
|
export declare function filterCovers(f: GrantFilter, scope: Scope): boolean;
|
|
99
|
+
/** True iff `p` covers a memory at `address` written by `author`. */
|
|
100
|
+
export declare function patternCovers(p: NodePattern, address: Address, author: Author): boolean;
|
|
101
|
+
/** True iff `g` may read `m` (by its address and author). */
|
|
102
|
+
export declare const grantReads: (g: Grant, m: Pick<Memory, 'scope' | 'address' | 'author'>) => boolean;
|
|
103
|
+
/** True iff `g` may change `m` (by its address and author). */
|
|
104
|
+
export declare const grantWrites: (g: Grant, m: Pick<Memory, 'scope' | 'address' | 'author'>) => boolean;
|
|
105
|
+
/** True iff `g` may read a Planet memory with `scope` (`scope.agentId` as its author). */
|
|
55
106
|
export declare const canRead: (g: Grant, scope: Scope) => boolean;
|
|
107
|
+
/** True iff `g` may change a Planet memory with `scope` (`scope.agentId` as its author). */
|
|
56
108
|
export declare const canWrite: (g: Grant, scope: Scope) => boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Two records occupy the same place: the same address, the same author and
|
|
111
|
+
* the same scope (extra scope fields included). A write that would change any
|
|
112
|
+
* of them for an existing id is refused.
|
|
113
|
+
*/
|
|
114
|
+
export declare function sameRecordAddress(a: Pick<Memory, 'scope' | 'address' | 'author'>, b: Pick<Memory, 'scope' | 'address' | 'author'>): boolean;
|
|
115
|
+
/** The Planet node of a principal: what pre-level grants cover. */
|
|
116
|
+
export declare function planetNode(p: Pick<Principal, 'orgId' | 'customerId'>): NodeRef;
|
|
57
117
|
/**
|
|
58
118
|
* The key a store keeps a cursor under. The local principal (the CLI's own,
|
|
59
119
|
* `local`/`self`/`claude-code`) keeps the bare key, so cursors written before
|
package/dist/grant.js
CHANGED
|
@@ -1,33 +1,48 @@
|
|
|
1
|
+
import { addressOf, assertNode, authorOf, nodeKey, nodeOf, planetOf, sameAddress, sameAuthor, } from './address.js';
|
|
1
2
|
/**
|
|
2
3
|
* The permission every `MemoryStore` method takes. Opaque: built only by
|
|
3
|
-
* `localGrant` or `
|
|
4
|
+
* `localGrant`, `principalGrant` or `nodeGrant`, frozen, and nominal, so a raw
|
|
4
5
|
* `ScopeFilter` (or any look-alike object) is rejected by the type checker
|
|
5
6
|
* and, for JavaScript callers, by `assertGrant` inside every store.
|
|
6
7
|
*
|
|
7
|
-
* `
|
|
8
|
-
* `coldGet`, `recordAccess`); `
|
|
9
|
-
* `forget`, `setTier`, `expireMany`, `coldPut`).
|
|
10
|
-
*
|
|
8
|
+
* `readNodes` bounds what can be seen (`get`, `lexical`, `dense`, `list`,
|
|
9
|
+
* `coldGet`, `recordAccess`); `writeNodes` bounds what can change (`upsert`,
|
|
10
|
+
* `forget`, `setTier`, `expireMany`, `coldPut`). A memory is covered when
|
|
11
|
+
* any pattern of the side covers its address and author. Cursors belong to
|
|
12
|
+
* the principal. Anything outside a grant behaves exactly like something that
|
|
11
13
|
* does not exist.
|
|
12
14
|
*/
|
|
13
15
|
export class Grant {
|
|
14
16
|
#issued = true;
|
|
15
17
|
principal;
|
|
18
|
+
/** @deprecated The pre-level view of `readNodes`; see `GrantFilter`. */
|
|
16
19
|
read;
|
|
20
|
+
/** @deprecated The pre-level view of `writeNodes`; see `GrantFilter`. */
|
|
17
21
|
write;
|
|
18
|
-
|
|
22
|
+
readNodes;
|
|
23
|
+
writeNodes;
|
|
24
|
+
constructor(principal, readNodes, writeNodes) {
|
|
19
25
|
this.principal = Object.freeze({ ...principal });
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
26
|
+
this.readNodes = Object.freeze(readNodes.map(freezePattern));
|
|
27
|
+
this.writeNodes = Object.freeze(writeNodes.map(freezePattern));
|
|
28
|
+
this.read = Object.freeze({
|
|
29
|
+
...principal,
|
|
30
|
+
projectIds: legacyProjects(principal, this.readNodes),
|
|
31
|
+
});
|
|
32
|
+
this.write = Object.freeze({
|
|
33
|
+
...principal,
|
|
34
|
+
projectIds: legacyProjects(principal, this.writeNodes),
|
|
35
|
+
});
|
|
22
36
|
Object.freeze(this);
|
|
23
37
|
}
|
|
24
38
|
/** @internal Use `localGrant` or `principalGrant`. */
|
|
25
39
|
static issue(principal, read, write) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
checkPrincipal(principal);
|
|
41
|
+
return new Grant(principal, [ownPlanet(principal, read)], [ownPlanet(principal, write)]);
|
|
42
|
+
}
|
|
43
|
+
/** @internal Use `nodeGrant`. */
|
|
44
|
+
static issueNodes(principal, read, write) {
|
|
45
|
+
checkPrincipal(principal);
|
|
31
46
|
return new Grant(principal, read, write);
|
|
32
47
|
}
|
|
33
48
|
/** True for a real grant (not a copy, not a look-alike). */
|
|
@@ -35,6 +50,51 @@ export class Grant {
|
|
|
35
50
|
return typeof x === 'object' && x !== null && #issued in x;
|
|
36
51
|
}
|
|
37
52
|
}
|
|
53
|
+
function checkPrincipal(principal) {
|
|
54
|
+
for (const k of ['orgId', 'customerId', 'agentId']) {
|
|
55
|
+
if (typeof principal[k] !== 'string' || principal[k] === '') {
|
|
56
|
+
throw new RangeError(`grant: ${k} must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** The pre-level grant side: the principal's own Planet, written by its own agent. */
|
|
61
|
+
function ownPlanet(p, projectIds) {
|
|
62
|
+
return {
|
|
63
|
+
node: { level: 'user', orgId: p.orgId, customerId: p.customerId },
|
|
64
|
+
projectIds,
|
|
65
|
+
authorAgentId: p.agentId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function freezePattern(p) {
|
|
69
|
+
if (typeof p !== 'object' || p === null)
|
|
70
|
+
throw new TypeError('grant: a node pattern must be an object');
|
|
71
|
+
assertNode(p.node);
|
|
72
|
+
const { level: _l, ...rest } = p.node;
|
|
73
|
+
const node = Object.freeze({ level: p.node.level, ...rest });
|
|
74
|
+
if (p.authorAgentId !== undefined &&
|
|
75
|
+
(typeof p.authorAgentId !== 'string' || p.authorAgentId === '')) {
|
|
76
|
+
throw new RangeError('grant: authorAgentId must be a non-empty string');
|
|
77
|
+
}
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
node,
|
|
80
|
+
projectIds: freezeProjects(p.projectIds),
|
|
81
|
+
...(p.authorAgentId !== undefined && { authorAgentId: p.authorAgentId }),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The projects of `patterns` that a pre-level reader may assume: those of
|
|
86
|
+
* patterns naming exactly the principal's Planet, restricted to its agent.
|
|
87
|
+
* Never wider than the patterns themselves.
|
|
88
|
+
*/
|
|
89
|
+
function legacyProjects(p, patterns) {
|
|
90
|
+
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);
|
|
92
|
+
if (mine.some((x) => x.projectIds === 'all'))
|
|
93
|
+
return 'all';
|
|
94
|
+
return Object.freeze([
|
|
95
|
+
...new Set(mine.flatMap((x) => x.projectIds)),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
38
98
|
function freezeProjects(p) {
|
|
39
99
|
if (p === 'all')
|
|
40
100
|
return 'all';
|
|
@@ -70,21 +130,70 @@ export function localGrant(read, opts = {}) {
|
|
|
70
130
|
export function principalGrant(p) {
|
|
71
131
|
return Grant.issue(p, 'all', 'all');
|
|
72
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* A grant over explicit node patterns, possibly across owners and levels:
|
|
135
|
+
* `read` bounds what is seen, `write` (default: `read`) what may change.
|
|
136
|
+
* Trusted: whoever builds one has already decided the principal may use
|
|
137
|
+
* every node it names (local configuration, or membership checked by a
|
|
138
|
+
* control plane). Nothing in the engine builds one for its own callers yet:
|
|
139
|
+
* the CLI, hooks and MCP server keep the pre-level grants.
|
|
140
|
+
*/
|
|
141
|
+
export function nodeGrant(opts) {
|
|
142
|
+
if (!Array.isArray(opts.read) || (opts.write !== undefined && !Array.isArray(opts.write))) {
|
|
143
|
+
throw new TypeError('nodeGrant: read and write must be arrays of node patterns');
|
|
144
|
+
}
|
|
145
|
+
return Grant.issueNodes(opts.principal, opts.read, opts.write ?? opts.read);
|
|
146
|
+
}
|
|
73
147
|
/** Rejects anything that is not a real `Grant`; every store calls it first. */
|
|
74
148
|
export function assertGrant(g) {
|
|
75
149
|
if (!Grant.is(g)) {
|
|
76
|
-
throw new TypeError('MemoryStore methods take a Grant (localGrant or
|
|
150
|
+
throw new TypeError('MemoryStore methods take a Grant (localGrant, principalGrant or nodeGrant)');
|
|
77
151
|
}
|
|
78
152
|
}
|
|
79
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* True iff a memory with `scope` lies inside `f`. Extra scope fields do not widen or narrow it.
|
|
155
|
+
* @deprecated Use `grantReads` / `grantWrites`.
|
|
156
|
+
*/
|
|
80
157
|
export function filterCovers(f, scope) {
|
|
81
158
|
return (scope.orgId === f.orgId &&
|
|
82
159
|
scope.customerId === f.customerId &&
|
|
83
160
|
scope.agentId === f.agentId &&
|
|
84
161
|
(f.projectIds === 'all' || f.projectIds.includes(scope.projectId ?? null)));
|
|
85
162
|
}
|
|
86
|
-
|
|
87
|
-
export
|
|
163
|
+
/** True iff `p` covers a memory at `address` written by `author`. */
|
|
164
|
+
export function patternCovers(p, address, author) {
|
|
165
|
+
return (nodeKey(nodeOf(address)) === nodeKey(p.node) &&
|
|
166
|
+
(p.projectIds === 'all' || p.projectIds.includes(address.projectId ?? null)) &&
|
|
167
|
+
(p.authorAgentId === undefined || p.authorAgentId === author.agentId));
|
|
168
|
+
}
|
|
169
|
+
function covers(patterns, m) {
|
|
170
|
+
const address = addressOf(m);
|
|
171
|
+
const author = authorOf(m);
|
|
172
|
+
return patterns.some((p) => patternCovers(p, address, author));
|
|
173
|
+
}
|
|
174
|
+
/** True iff `g` may read `m` (by its address and author). */
|
|
175
|
+
export const grantReads = (g, m) => covers(g.readNodes, m);
|
|
176
|
+
/** True iff `g` may change `m` (by its address and author). */
|
|
177
|
+
export const grantWrites = (g, m) => covers(g.writeNodes, m);
|
|
178
|
+
/** True iff `g` may read a Planet memory with `scope` (`scope.agentId` as its author). */
|
|
179
|
+
export const canRead = (g, scope) => grantReads(g, { scope });
|
|
180
|
+
/** True iff `g` may change a Planet memory with `scope` (`scope.agentId` as its author). */
|
|
181
|
+
export const canWrite = (g, scope) => grantWrites(g, { scope });
|
|
182
|
+
/**
|
|
183
|
+
* Two records occupy the same place: the same address, the same author and
|
|
184
|
+
* the same scope (extra scope fields included). A write that would change any
|
|
185
|
+
* of them for an existing id is refused.
|
|
186
|
+
*/
|
|
187
|
+
export function sameRecordAddress(a, b) {
|
|
188
|
+
return (sameAddress(addressOf(a), addressOf(b)) &&
|
|
189
|
+
sameAuthor(authorOf(a), authorOf(b)) &&
|
|
190
|
+
sameScope(a.scope, b.scope));
|
|
191
|
+
}
|
|
192
|
+
/** The Planet node of a principal: what pre-level grants cover. */
|
|
193
|
+
export function planetNode(p) {
|
|
194
|
+
const { projectId: _p, ...node } = planetOf({ ...p, agentId: '-' });
|
|
195
|
+
return node;
|
|
196
|
+
}
|
|
88
197
|
/**
|
|
89
198
|
* The key a store keeps a cursor under. The local principal (the CLI's own,
|
|
90
199
|
* `local`/`self`/`claude-code`) keeps the bare key, so cursors written before
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.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';
|
|
@@ -14,6 +16,7 @@ export * from './recall.js';
|
|
|
14
16
|
export * from './relevance.js';
|
|
15
17
|
export * from './observe.js';
|
|
16
18
|
export * from './temporal.js';
|
|
19
|
+
export { chronoLoaded, mayHaveDateCue, preloadChrono } from './date-cue.js';
|
|
17
20
|
export * from './volatile.js';
|
|
18
21
|
export * from './scrub.js';
|
|
19
22
|
export * from './cap.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.3.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';
|
|
@@ -14,6 +16,7 @@ export * from './recall.js';
|
|
|
14
16
|
export * from './relevance.js';
|
|
15
17
|
export * from './observe.js';
|
|
16
18
|
export * from './temporal.js';
|
|
19
|
+
export { chronoLoaded, mayHaveDateCue, preloadChrono } from './date-cue.js';
|
|
17
20
|
export * from './volatile.js';
|
|
18
21
|
export * from './scrub.js';
|
|
19
22
|
export * from './cap.js';
|
package/dist/isolation.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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';
|
|
@@ -11,6 +12,12 @@ import type { Memory, Scope } from './model.js';
|
|
|
11
12
|
export declare function isoMemory(id: string, scope: Scope, marker: string, embedding?: Int8Array): Memory;
|
|
12
13
|
/** The narrowest grant for one scope (its principal and its one project or global). */
|
|
13
14
|
export declare function soleGrant(s: Scope): Grant;
|
|
15
|
+
/** The narrowest grant for one memory at any level: its node, its one project, its author. */
|
|
16
|
+
export declare function soleGrantFor(m: Pick<Memory, 'scope' | 'address' | 'author'>): Grant;
|
|
17
|
+
/** `m` moved to `scope`: its address and author dropped, so the scope's Planet is the address. */
|
|
18
|
+
export declare function movedTo(m: Memory, scope: Scope): Memory;
|
|
19
|
+
/** A memory at `address`, written by `author`, for the isolation checks. */
|
|
20
|
+
export declare function isoAt(id: string, address: Address, author: Author, marker: string, embedding?: Int8Array): Memory;
|
|
14
21
|
/** The scopes the checks use: the caller, and victims that differ from it in one coordinate each. */
|
|
15
22
|
export declare const ISO_SCOPES: {
|
|
16
23
|
mine: {
|
|
@@ -49,6 +56,16 @@ export declare const ISO_SCOPES: {
|
|
|
49
56
|
projectId: string;
|
|
50
57
|
};
|
|
51
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Victims at the other levels, each next to the caller's own Planet: a Planet
|
|
61
|
+
* memory another of the caller's agents wrote, the caller's agent's own Star,
|
|
62
|
+
* an Orbit, a Flyby, a Constellation and a Galaxy root of an organisation, and
|
|
63
|
+
* one of the caller's own owner. A pre-level grant sees none of them.
|
|
64
|
+
*/
|
|
65
|
+
export declare const ISO_LEVELS: Record<string, {
|
|
66
|
+
address: Address;
|
|
67
|
+
author: Author;
|
|
68
|
+
}>;
|
|
52
69
|
/**
|
|
53
70
|
* Every `MemoryStore` method, called with a grant for `ISO_SCOPES.mine`
|
|
54
71
|
* against memories (warm and cold) in each other scope: nothing is seen,
|
|
@@ -73,5 +90,11 @@ export declare function seededRng(seed: number): {
|
|
|
73
90
|
* order: SQLite FTS5's bm25 uses table-wide term statistics, so another
|
|
74
91
|
* tenant's text shifts scores (a known side channel; see the PR notes).
|
|
75
92
|
* Every other result is compared exactly, dense scores included.
|
|
93
|
+
*
|
|
94
|
+
* `multiNode`: A's grant spans its Planet, its agent's Star and a
|
|
95
|
+
* Constellation of another owner; B's spans its own Planet and a sibling
|
|
96
|
+
* Constellation. Same property.
|
|
76
97
|
*/
|
|
77
|
-
export declare function nonInterference(make: () => Promise<MemoryStore>, seed: number, steps: number
|
|
98
|
+
export declare function nonInterference(make: () => Promise<MemoryStore>, seed: number, steps: number, opts?: {
|
|
99
|
+
multiNode?: boolean;
|
|
100
|
+
}): Promise<string | null>;
|