@melaya/runner 1.1.61 → 1.1.63
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/assistantFiles.d.ts +29 -0
- package/dist/assistantFiles.js +113 -0
- package/dist/assistantHost.py +76 -1
- package/dist/cli.js +7 -0
- package/dist/connection.js +146 -55
- package/dist/folderPicker.d.ts +24 -0
- package/dist/folderPicker.js +139 -0
- package/dist/localRelay.d.ts +8 -1
- package/dist/localRelay.js +21 -1
- package/dist/localVault.py +745 -0
- package/dist/localVaultMirror.py +507 -0
- package/dist/obsidianApp.d.ts +324 -0
- package/dist/obsidianApp.js +1143 -0
- package/dist/obsidianAppCli.d.ts +15 -0
- package/dist/obsidianAppCli.js +123 -0
- package/dist/vaultMirror.d.ts +104 -0
- package/dist/vaultMirror.js +305 -0
- package/dist/vaults.d.ts +350 -0
- package/dist/vaults.js +1130 -0
- package/localVault.py +745 -0
- package/localVaultMirror.py +507 -0
- package/package.json +4 -2
package/dist/vaults.d.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Obsidian vault host — the runner side of docs/plans/obsidian-vault-protocol.md.
|
|
3
|
+
*
|
|
4
|
+
* A "device vault" is a folder on THIS computer, registered by the server for
|
|
5
|
+
* this runner's stable install id (rnr_…). The runner:
|
|
6
|
+
* - keeps the registrations in ~/.melaya-runner/vaults/registry.json (0600)
|
|
7
|
+
* and re-opens them on start (incremental re-index);
|
|
8
|
+
* - indexes and serves them through a long-lived Python host (localVault.py,
|
|
9
|
+
* the pure-Python engine from the shared bundle), index outside the vault
|
|
10
|
+
* at ~/.melaya-runner/vaults/<vault_id>/index.sqlite;
|
|
11
|
+
* - verifies every vault capability ITSELF (Ed25519, keys pushed with
|
|
12
|
+
* runner:vaultKeys): signature, exp, aud, grant selection, read/write,
|
|
13
|
+
* route policy, pushed revocations, folder bounds. The server relay is
|
|
14
|
+
* not trusted to bypass any of it;
|
|
15
|
+
* - answers server requests on the socket (runner:vault-*) and local agent
|
|
16
|
+
* requests on the local relay (POST /vaults/op), forwarding every vault it
|
|
17
|
+
* does not hold to {server}/api/v1/vault-api/op.
|
|
18
|
+
*
|
|
19
|
+
* Never logged: capability tokens, note content, op arguments.
|
|
20
|
+
*/
|
|
21
|
+
import { type KeyObject } from "crypto";
|
|
22
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
23
|
+
import { type ObsidianAppBridge } from "./obsidianApp.js";
|
|
24
|
+
import { VaultMirrorManager } from "./vaultMirror.js";
|
|
25
|
+
export declare const VAULT_HOST_CAPABILITY = "vault_host_v1";
|
|
26
|
+
/** Engine methods that change the vault. Everything else is a read. A write
|
|
27
|
+
* needs "write" in the grant's ops; an unknown name stays a read here and the
|
|
28
|
+
* Python host still refuses names that are not public engine methods. The
|
|
29
|
+
* host applies the engine's own proposal in the same request (localVault.py
|
|
30
|
+
* cmd_op), so the list must match its WRITE_OPS. */
|
|
31
|
+
export declare const VAULT_WRITE_OPS: ReadonlySet<string>;
|
|
32
|
+
/** Engine methods no caller may name: `apply` / `resume_recovery` take a
|
|
33
|
+
* caller-supplied proposal (forged bytes), the rest are host lifecycle. */
|
|
34
|
+
export declare const VAULT_DENIED_OPS: ReadonlySet<string>;
|
|
35
|
+
export type VaultErrorCode = "not_selected" | "forbidden" | "forbidden_folder" | "vault_offline" | "app_offline" | "stale" | "not_found" | "ambiguous" | "conflict" | "unsupported" | "quota" | "policy_route" | "cursor_expired" | "invalid";
|
|
36
|
+
export interface VaultEnvelopeError {
|
|
37
|
+
ok: false;
|
|
38
|
+
error: {
|
|
39
|
+
code: VaultErrorCode;
|
|
40
|
+
message: string;
|
|
41
|
+
retry_after: number | null;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export type VaultEnvelope = VaultEnvelopeError | ({
|
|
45
|
+
ok: true;
|
|
46
|
+
} & Record<string, unknown>);
|
|
47
|
+
export declare function vaultErr(code: VaultErrorCode, message: string): VaultEnvelopeError;
|
|
48
|
+
export interface CapGrant {
|
|
49
|
+
vault_id: string;
|
|
50
|
+
grant_id: string;
|
|
51
|
+
ops: Array<"read" | "write">;
|
|
52
|
+
folders: string[];
|
|
53
|
+
policy_rev: number;
|
|
54
|
+
revocation_version: number;
|
|
55
|
+
source: "device" | "upload" | "git" | "sync";
|
|
56
|
+
rnr_id: string | null;
|
|
57
|
+
storage: "device_only" | "mirror";
|
|
58
|
+
relay: boolean;
|
|
59
|
+
external_models: boolean;
|
|
60
|
+
history: "reference" | "retain";
|
|
61
|
+
label: string;
|
|
62
|
+
default: boolean;
|
|
63
|
+
/** Vault policy "Developer tools" (obsidian_eval, dev:*): off unless true. */
|
|
64
|
+
dev_tools?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface CapPayload {
|
|
67
|
+
v: 1;
|
|
68
|
+
kid: string;
|
|
69
|
+
jti: string;
|
|
70
|
+
sub: string;
|
|
71
|
+
act: string;
|
|
72
|
+
aud: string[];
|
|
73
|
+
iat: number;
|
|
74
|
+
exp: number;
|
|
75
|
+
route: "cloud" | "device";
|
|
76
|
+
models: Array<"external" | "local">;
|
|
77
|
+
grants: CapGrant[];
|
|
78
|
+
}
|
|
79
|
+
export interface VaultRegistration {
|
|
80
|
+
id: string;
|
|
81
|
+
label: string;
|
|
82
|
+
path: string;
|
|
83
|
+
configDirName: string;
|
|
84
|
+
include: string[];
|
|
85
|
+
exclude: string[];
|
|
86
|
+
}
|
|
87
|
+
/** Where a request came from: the server relay (runner:vault-op) or a local
|
|
88
|
+
* agent through the 127.0.0.1 relay. Route policy differs (relay=true). */
|
|
89
|
+
export type VaultVia = "server" | "local";
|
|
90
|
+
/** kid → Ed25519 public key, from runner:vaultKeys {keys:[{kid, publicKeyRawB64}]}. */
|
|
91
|
+
export declare function parseVaultKeys(keys: unknown): Map<string, KeyObject>;
|
|
92
|
+
/** Decode the payload WITHOUT verifying it. Only for routing a local relay
|
|
93
|
+
* request (local vs forward); every served request is fully verified. */
|
|
94
|
+
export declare function decodeVaultCapUnverified(token: string): CapPayload | null;
|
|
95
|
+
export type CapCheck = {
|
|
96
|
+
ok: true;
|
|
97
|
+
payload: CapPayload;
|
|
98
|
+
} | {
|
|
99
|
+
ok: false;
|
|
100
|
+
code: VaultErrorCode;
|
|
101
|
+
reason: string;
|
|
102
|
+
};
|
|
103
|
+
/** Record a run or turn capability received from the server for a local agent. */
|
|
104
|
+
export declare function trustRunVaultCapability(token: string | null | undefined): void;
|
|
105
|
+
export declare function isTrustedRunVaultCapability(token: string): boolean;
|
|
106
|
+
/** Signature, version, kid, lifetime and audience. `audiences` is any-of. `maxLifetimeS` is raised only for
|
|
107
|
+
* run/turn capabilities the server handed to this runner (see above). */
|
|
108
|
+
export declare function verifyVaultCapability(token: string, keys: Map<string, KeyObject>, audiences: string[], nowSec?: number, maxLifetimeS?: number): CapCheck;
|
|
109
|
+
export type GrantPick = {
|
|
110
|
+
ok: true;
|
|
111
|
+
grant: CapGrant;
|
|
112
|
+
} | {
|
|
113
|
+
ok: false;
|
|
114
|
+
error: VaultEnvelopeError;
|
|
115
|
+
};
|
|
116
|
+
/** `vault` = a vlt_ id in grants, or the exact case-insensitive label of exactly
|
|
117
|
+
* one grant. Empty = the grant marked default, or the only grant. */
|
|
118
|
+
export declare function selectVaultGrant(payload: Pick<CapPayload, "grants">, vault: string | null | undefined): GrantPick;
|
|
119
|
+
export declare function isVaultWriteOp(op: string): boolean;
|
|
120
|
+
/** vaultId+grantId → the minimum revocation_version still valid. A grant whose
|
|
121
|
+
* capability carries an older version fails the next call, whatever the
|
|
122
|
+
* token's remaining lifetime. Process memory only: a restarted runner relies
|
|
123
|
+
* on the short capability lifetime plus the server re-pushing. */
|
|
124
|
+
export declare class VaultRevocations {
|
|
125
|
+
private readonly min;
|
|
126
|
+
revoke(vaultId: string, grantId: string, revocationVersion: number): void;
|
|
127
|
+
isRevoked(g: Pick<CapGrant, "vault_id" | "grant_id" | "revocation_version">): boolean;
|
|
128
|
+
}
|
|
129
|
+
/** Steps 2-5 of the enforcement order for one already-verified capability.
|
|
130
|
+
* Returns the grant to serve, or the error envelope. */
|
|
131
|
+
export declare function authorizeVaultOp(payload: CapPayload, input: {
|
|
132
|
+
vault: string | null | undefined;
|
|
133
|
+
op: string;
|
|
134
|
+
}, ctx: {
|
|
135
|
+
via: VaultVia;
|
|
136
|
+
revocations: VaultRevocations;
|
|
137
|
+
}): {
|
|
138
|
+
ok: true;
|
|
139
|
+
grant: CapGrant;
|
|
140
|
+
} | {
|
|
141
|
+
ok: false;
|
|
142
|
+
error: VaultEnvelopeError;
|
|
143
|
+
};
|
|
144
|
+
export type FolderPolicyError = "forbidden_path" | "not_a_folder";
|
|
145
|
+
export interface FolderPolicyOpts {
|
|
146
|
+
platform?: NodeJS.Platform;
|
|
147
|
+
home?: string;
|
|
148
|
+
runnerDir?: string;
|
|
149
|
+
/** Extra absolute folders refused with all of their contents. */
|
|
150
|
+
extraForbidden?: string[];
|
|
151
|
+
}
|
|
152
|
+
/** True iff `child` is `parent` or inside it. */
|
|
153
|
+
export declare function isInsidePath(platform: NodeJS.Platform, child: string, parent: string): boolean;
|
|
154
|
+
/** Pure check of an already-resolved real path. */
|
|
155
|
+
export declare function checkVaultFolderPolicy(real: string, opts?: FolderPolicyOpts): FolderPolicyError | null;
|
|
156
|
+
/** Resolve a picked folder to the real path the engine will serve, refusing
|
|
157
|
+
* symlink / junction roots, non-directories and protected locations. */
|
|
158
|
+
export declare function resolveVaultFolder(input: string, opts?: FolderPolicyOpts): {
|
|
159
|
+
ok: true;
|
|
160
|
+
real: string;
|
|
161
|
+
} | {
|
|
162
|
+
ok: false;
|
|
163
|
+
error: FolderPolicyError;
|
|
164
|
+
};
|
|
165
|
+
export declare function defaultVaultsDir(): string;
|
|
166
|
+
/** Validate a registration coming from the server. Path policy is separate. */
|
|
167
|
+
export declare function normalizeRegistration(v: unknown): VaultRegistration | null;
|
|
168
|
+
export declare class VaultRegistry {
|
|
169
|
+
readonly dir: string;
|
|
170
|
+
readonly file: string;
|
|
171
|
+
private vaults;
|
|
172
|
+
constructor(dir?: string);
|
|
173
|
+
load(): void;
|
|
174
|
+
list(): VaultRegistration[];
|
|
175
|
+
get(id: string): VaultRegistration | undefined;
|
|
176
|
+
upsert(reg: VaultRegistration): void;
|
|
177
|
+
remove(id: string): boolean;
|
|
178
|
+
/** Index folder of one vault (outside the vault). */
|
|
179
|
+
indexDir(id: string): string;
|
|
180
|
+
private save;
|
|
181
|
+
}
|
|
182
|
+
export interface VaultHostStatusEvent {
|
|
183
|
+
vaultId: string;
|
|
184
|
+
status: string;
|
|
185
|
+
notes?: number;
|
|
186
|
+
bytes?: number;
|
|
187
|
+
indexRev?: number;
|
|
188
|
+
error?: string;
|
|
189
|
+
capabilities?: unknown;
|
|
190
|
+
}
|
|
191
|
+
export interface VaultHostTransport {
|
|
192
|
+
request(cmd: string, payload: Record<string, unknown>, timeoutMs?: number): Promise<any>;
|
|
193
|
+
onEvent(cb: (ev: VaultHostStatusEvent) => void): void;
|
|
194
|
+
/** Called after the host process (re)started, to re-register vaults. */
|
|
195
|
+
onRestart(cb: () => void): void;
|
|
196
|
+
stop(): void;
|
|
197
|
+
}
|
|
198
|
+
export interface PythonLaunch {
|
|
199
|
+
pythonPath: string;
|
|
200
|
+
scriptPath: string;
|
|
201
|
+
env: Record<string, string>;
|
|
202
|
+
cwd: string;
|
|
203
|
+
}
|
|
204
|
+
/** JSON-lines RPC to one long-lived `python -u localVault.py`, started lazily
|
|
205
|
+
* and restarted with backoff when it dies while vaults are registered. */
|
|
206
|
+
export declare class VaultHostProcess implements VaultHostTransport {
|
|
207
|
+
private readonly o;
|
|
208
|
+
private proc;
|
|
209
|
+
private starting;
|
|
210
|
+
private seq;
|
|
211
|
+
private buf;
|
|
212
|
+
private readonly pending;
|
|
213
|
+
private readonly eventCbs;
|
|
214
|
+
private readonly restartCbs;
|
|
215
|
+
private backoffMs;
|
|
216
|
+
private stopped;
|
|
217
|
+
private spawns;
|
|
218
|
+
private restartTimer;
|
|
219
|
+
readonly stderrTail: string[];
|
|
220
|
+
constructor(o: {
|
|
221
|
+
launch: () => Promise<PythonLaunch>;
|
|
222
|
+
/** Whether a crashed host should be restarted (vaults still registered). */
|
|
223
|
+
wanted: () => boolean;
|
|
224
|
+
log?: (m: string) => void;
|
|
225
|
+
});
|
|
226
|
+
onEvent(cb: (ev: VaultHostStatusEvent) => void): void;
|
|
227
|
+
onRestart(cb: () => void): void;
|
|
228
|
+
private ensure;
|
|
229
|
+
private onStdout;
|
|
230
|
+
private onExit;
|
|
231
|
+
request(cmd: string, payload: Record<string, unknown>, timeoutMs?: number): Promise<any>;
|
|
232
|
+
stop(): void;
|
|
233
|
+
}
|
|
234
|
+
export type VaultResult = {
|
|
235
|
+
ok: true;
|
|
236
|
+
data?: unknown;
|
|
237
|
+
envelope?: unknown;
|
|
238
|
+
} | {
|
|
239
|
+
ok: false;
|
|
240
|
+
error: string;
|
|
241
|
+
};
|
|
242
|
+
export interface VaultServiceOpts {
|
|
243
|
+
rnrId: string;
|
|
244
|
+
registry: VaultRegistry;
|
|
245
|
+
host: VaultHostTransport;
|
|
246
|
+
/** http(s) base of the Melaya server (for forwarding non-local vaults). */
|
|
247
|
+
serverHttpBase: string;
|
|
248
|
+
emitStatus?: (ev: VaultHostStatusEvent) => void;
|
|
249
|
+
log?: (m: string) => void;
|
|
250
|
+
fetchImpl?: typeof fetch;
|
|
251
|
+
folderPolicy?: FolderPolicyOpts;
|
|
252
|
+
/** fs.watch the registered folders (off in tests). */
|
|
253
|
+
watch?: boolean;
|
|
254
|
+
/** The Obsidian app bridge (obsidianApp.ts): App ops through the user's
|
|
255
|
+
* running Obsidian. Absent = App ops are unsupported on this runner. */
|
|
256
|
+
app?: ObsidianAppBridge;
|
|
257
|
+
}
|
|
258
|
+
export declare class VaultService {
|
|
259
|
+
private readonly o;
|
|
260
|
+
private keys;
|
|
261
|
+
readonly revocations: VaultRevocations;
|
|
262
|
+
private readonly cursorKey;
|
|
263
|
+
private readonly watchers;
|
|
264
|
+
private readonly lastStatus;
|
|
265
|
+
/** Cloud copy of `storage: mirror` vaults (vaultMirror.ts, plan §6.4 S4). */
|
|
266
|
+
readonly mirror: VaultMirrorManager;
|
|
267
|
+
private mirrorSink;
|
|
268
|
+
constructor(o: VaultServiceOpts);
|
|
269
|
+
/** Where mirror renew requests and statuses go (the runner socket). */
|
|
270
|
+
setMirrorSink(sink: (event: string, payload: Record<string, unknown>) => void): void;
|
|
271
|
+
get rnrId(): string;
|
|
272
|
+
hasKeys(): boolean;
|
|
273
|
+
/** runner:vaultKeys — replaces the key set (the server sends all current kids). */
|
|
274
|
+
setKeys(keys: unknown): void;
|
|
275
|
+
/** runner:vaultRevoke */
|
|
276
|
+
revoke(p: {
|
|
277
|
+
vaultId?: unknown;
|
|
278
|
+
grantId?: unknown;
|
|
279
|
+
revocationVersion?: unknown;
|
|
280
|
+
}): void;
|
|
281
|
+
/** Statuses to re-send after a reconnect. */
|
|
282
|
+
latestStatuses(): VaultHostStatusEvent[];
|
|
283
|
+
private hostRegisterPayload;
|
|
284
|
+
/** runner:vault-register */
|
|
285
|
+
register(input: unknown): Promise<VaultResult>;
|
|
286
|
+
/** runner:vault-unregister — also deletes the derived index (it holds note text). */
|
|
287
|
+
unregister(vaultId: unknown): Promise<VaultResult>;
|
|
288
|
+
/** Re-open every registered vault (startup, host restart). */
|
|
289
|
+
restore(): Promise<void>;
|
|
290
|
+
/** Verify a capability and serve one op for a vault held on THIS runner. */
|
|
291
|
+
op(input: {
|
|
292
|
+
capability?: unknown;
|
|
293
|
+
op?: unknown;
|
|
294
|
+
vault?: unknown;
|
|
295
|
+
args?: unknown;
|
|
296
|
+
idempotency_key?: unknown;
|
|
297
|
+
}, via: VaultVia): Promise<VaultEnvelope>;
|
|
298
|
+
/** One engine op on the Python host (re-registers once after a host restart). */
|
|
299
|
+
private hostOp;
|
|
300
|
+
/** Merge the Obsidian app's availability into a vault's capability report
|
|
301
|
+
* (stored by the server as agents.vaults.capabilities). */
|
|
302
|
+
private withAppCapabilities;
|
|
303
|
+
/** runner:vault-app — the owner's vault settings asking about / connecting
|
|
304
|
+
* the Obsidian app on this computer. The Local REST API key is read from
|
|
305
|
+
* the plugin's own settings HERE and stays here: only states and a short
|
|
306
|
+
* certificate fingerprint go back. */
|
|
307
|
+
appControl(p: {
|
|
308
|
+
vaultId?: unknown;
|
|
309
|
+
action?: unknown;
|
|
310
|
+
}): Promise<VaultResult>;
|
|
311
|
+
/** True iff a relay request targets a device vault held by this runner. */
|
|
312
|
+
isLocalTarget(capability: string, vault: unknown): boolean;
|
|
313
|
+
/** POST /vaults/op on the local relay (127.0.0.1). */
|
|
314
|
+
handleRelayHttp(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
315
|
+
private startWatch;
|
|
316
|
+
private stopWatch;
|
|
317
|
+
stop(): void;
|
|
318
|
+
}
|
|
319
|
+
/** The subset of a socket.io-client Socket used here (a fake in tests). */
|
|
320
|
+
export interface VaultSocketLike {
|
|
321
|
+
on(event: string, cb: (...args: any[]) => void): unknown;
|
|
322
|
+
emit(event: string, ...args: any[]): unknown;
|
|
323
|
+
}
|
|
324
|
+
/** Wire the server → runner vault events. Every request is answered with
|
|
325
|
+
* runner:vault-result {requestId, ok, data | envelope | error}. */
|
|
326
|
+
export declare function attachVaultSocketHandlers(socket: VaultSocketLike, service: VaultService, pickFolder: () => Promise<{
|
|
327
|
+
path: string;
|
|
328
|
+
} | {
|
|
329
|
+
cancelled: true;
|
|
330
|
+
}>): void;
|
|
331
|
+
/** Called once the local relay is listening (connection.ts). */
|
|
332
|
+
export declare function setVaultRelayPort(port: number): void;
|
|
333
|
+
/**
|
|
334
|
+
* Env a runner-hosted agent needs to call vault tools (protocol §5): the
|
|
335
|
+
* capability and the LOCAL relay URL, which serves this computer's vaults
|
|
336
|
+
* in-process and forwards every other vault to the server.
|
|
337
|
+
*
|
|
338
|
+
* Must be merged LAST into the subprocess env (after payload.credentials, so a
|
|
339
|
+
* credential named MEL_VAULT_* can never replace it) by:
|
|
340
|
+
* - the pipeline spawn in connection.ts `runner:run` (the env object built
|
|
341
|
+
* next to `PYTHONPATH: sharedDir` / MEL_STUDIO_URL), from the run payload's
|
|
342
|
+
* capability;
|
|
343
|
+
* - the assistant host, per turn, in `runner:assistant_turn` (the turn's
|
|
344
|
+
* capability; the host is long-lived, so the value rides the turn frame,
|
|
345
|
+
* not the boot env built by `buildAssistantEnv`).
|
|
346
|
+
* Returns {} when there is no capability or the relay is not up yet.
|
|
347
|
+
*/
|
|
348
|
+
export declare function vaultToolEnv(capability: string | null | undefined, relayPort?: number): Record<string, string>;
|
|
349
|
+
/** Locate localVault.py next to the compiled module (dist/) or one level up (src tree). */
|
|
350
|
+
export declare function findVaultHostScript(baseDir: string): string | null;
|