@animalabs/agent-framework 0.22.4 → 0.23.1
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/README.md +105 -3
- package/dist/src/agent.d.ts +36 -10
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +76 -32
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +34 -2
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +72 -16
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +3 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +47 -7
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +12 -7
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +59 -11
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +2 -2
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +6 -2
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/framework.d.ts +744 -120
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3546 -906
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +13 -6
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -0
- package/dist/src/held-writes.d.ts.map +1 -0
- package/dist/src/held-writes.js +56 -0
- package/dist/src/held-writes.js.map +1 -0
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +4 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +1 -1
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +95 -18
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +578 -150
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +2 -2
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +6 -7
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +4 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +6 -7
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +7 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +9 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +227 -0
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
- package/dist/src/mcpl/modern-connection.js +1088 -0
- package/dist/src/mcpl/modern-connection.js.map +1 -0
- package/dist/src/mcpl/prefix-rule.d.ts +18 -0
- package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
- package/dist/src/mcpl/prefix-rule.js +25 -0
- package/dist/src/mcpl/prefix-rule.js.map +1 -0
- package/dist/src/mcpl/protocol-family.d.ts +179 -0
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
- package/dist/src/mcpl/protocol-family.js +697 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -0
- package/dist/src/mcpl/push-coalescer.d.ts +38 -19
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +52 -23
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +66 -15
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +243 -76
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +63 -18
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +117 -32
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +326 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +995 -212
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +25 -5
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +51 -12
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.js +232 -0
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +56 -13
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +206 -70
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +66 -20
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +2 -1
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +2 -1
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +16 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +65 -6
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +3 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +42 -2
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +268 -65
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +27 -5
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +366 -76
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/refusal.d.ts +14 -0
- package/dist/src/modules/discord/refusal.d.ts.map +1 -0
- package/dist/src/modules/discord/refusal.js +14 -0
- package/dist/src/modules/discord/refusal.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +17 -7
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +6 -2
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +25 -2
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +45 -5
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +2 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +19 -9
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +214 -42
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +667 -113
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +240 -8
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +554 -24
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +10 -1
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +42 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +130 -25
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +112 -39
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +100 -29
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/tool-args.d.ts +40 -0
- package/dist/src/tool-args.d.ts.map +1 -0
- package/dist/src/tool-args.js +207 -0
- package/dist/src/tool-args.js.map +1 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +9 -2
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/types/agent.d.ts +27 -17
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +8 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +17 -2
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +54 -10
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/agent.ts +99 -36
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +104 -17
- package/src/code-execution/runtime-py.ts +47 -7
- package/src/code-execution/tool-definition.ts +65 -11
- package/src/config-checks.ts +4 -2
- package/src/framework.ts +3633 -956
- package/src/gate/event-gate.ts +14 -6
- package/src/held-writes.ts +59 -0
- package/src/index.ts +19 -4
- package/src/mcpl/capability-grant.ts +1 -1
- package/src/mcpl/channel-registry.ts +613 -160
- package/src/mcpl/convert-block.ts +15 -7
- package/src/mcpl/index.ts +14 -1
- package/src/mcpl/inference-router.ts +8 -7
- package/src/mcpl/log-value.ts +10 -0
- package/src/mcpl/modern-connection.ts +1182 -0
- package/src/mcpl/prefix-rule.ts +32 -0
- package/src/mcpl/protocol-family.ts +691 -0
- package/src/mcpl/push-coalescer.ts +67 -29
- package/src/mcpl/reference-fetcher.ts +288 -82
- package/src/mcpl/references.ts +123 -33
- package/src/mcpl/server-connection.ts +1050 -221
- package/src/mcpl/server-registry.ts +50 -12
- package/src/mcpl/tool-result-normalize.ts +294 -0
- package/src/mcpl/transport.ts +204 -62
- package/src/mcpl/types.ts +67 -20
- package/src/mcpl/visible-content.ts +2 -1
- package/src/module-registry.ts +68 -13
- package/src/modules/api/index.ts +3 -2
- package/src/modules/discord/discord-js-client.ts +288 -72
- package/src/modules/discord/index.ts +380 -77
- package/src/modules/discord/refusal.ts +13 -0
- package/src/modules/discord/types.ts +17 -9
- package/src/modules/health/index.ts +6 -2
- package/src/modules/index.ts +1 -1
- package/src/modules/workspace/disk-agreement.ts +46 -5
- package/src/modules/workspace/effects.ts +18 -7
- package/src/modules/workspace/index.ts +722 -124
- package/src/modules/workspace/observe.ts +591 -22
- package/src/modules/workspace/types.ts +10 -1
- package/src/modules/workspace/watcher.ts +126 -25
- package/src/silent-heartbeat.ts +184 -59
- package/src/tool-args.ts +213 -0
- package/src/tool-presentation.ts +9 -2
- package/src/types/agent.ts +27 -17
- package/src/types/events.ts +8 -0
- package/src/types/framework.ts +17 -2
- package/src/types/module.ts +55 -10
- package/src/types/trace.ts +2 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A refusal the Discord module gives the agent as worded: Discord refused the
|
|
3
|
+
* request, its target can't take it, or a lookup before the tool's change (a
|
|
4
|
+
* send, an edit, a deletion, a reaction or a thread) stopped on an unexpected
|
|
5
|
+
* error, so the tool did nothing, and the message says why and what to do
|
|
6
|
+
* instead. A thread a send asked for and didn't get isn't one: the send
|
|
7
|
+
* returns why as words, since its message is out.
|
|
8
|
+
* Anything else a tool throws is an unexpected error, which the module words
|
|
9
|
+
* itself (DiscordModule.handleToolCall).
|
|
10
|
+
*/
|
|
11
|
+
export class DiscordRefusal extends Error {
|
|
12
|
+
override readonly name = 'DiscordRefusal';
|
|
13
|
+
}
|
|
@@ -375,6 +375,10 @@ export interface DiscordEditEvent {
|
|
|
375
375
|
type: 'discord:edit';
|
|
376
376
|
discordMessageId: string;
|
|
377
377
|
newContent: string;
|
|
378
|
+
/** The message's attachments as the edit left them, when the client knows
|
|
379
|
+
* them: a stored row whose text comes first drops the blocks of those it
|
|
380
|
+
* no longer has. */
|
|
381
|
+
attachments?: DiscordAttachment[];
|
|
378
382
|
[key: string]: unknown;
|
|
379
383
|
}
|
|
380
384
|
|
|
@@ -412,10 +416,12 @@ export interface DiscordClientInterface {
|
|
|
412
416
|
|
|
413
417
|
/**
|
|
414
418
|
* Send a message to a channel, and start the thread `createThread` asks
|
|
415
|
-
* for when the channel can hold threads
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
+
* for when the channel can hold threads. When this resolves, the message
|
|
420
|
+
* was sent. `threadFailure`, when there is one, is why the thread it tried
|
|
421
|
+
* to start wasn't, which the send reports as its error; `note`, when there
|
|
422
|
+
* is one, is something the agent should know of a send that succeeded,
|
|
423
|
+
* such as a thread its channel can't hold. DiscordJsClient words both for
|
|
424
|
+
* the agent.
|
|
419
425
|
*/
|
|
420
426
|
sendMessage(
|
|
421
427
|
channelId: string,
|
|
@@ -424,10 +430,10 @@ export interface DiscordClientInterface {
|
|
|
424
430
|
replyTo?: string;
|
|
425
431
|
createThread?: { name: string };
|
|
426
432
|
}
|
|
427
|
-
): Promise<{ messageId: string; threadFailure?: string }>;
|
|
433
|
+
): Promise<{ messageId: string; threadFailure?: string; note?: string }>;
|
|
428
434
|
|
|
429
|
-
/** Send a DM to a user */
|
|
430
|
-
sendDM(userId: string, content: string): Promise<{ messageId: string }>;
|
|
435
|
+
/** Send a DM to a user. `channelId`, when the client gives it, is the direct message channel's id, which history reads. */
|
|
436
|
+
sendDM(userId: string, content: string): Promise<{ messageId: string; channelId?: string }>;
|
|
431
437
|
|
|
432
438
|
/** Fetch a message's text content */
|
|
433
439
|
fetchMessage(channelId: string, messageId: string): Promise<string>;
|
|
@@ -467,8 +473,10 @@ export interface DiscordClientInterface {
|
|
|
467
473
|
/** Register message handler */
|
|
468
474
|
onMessage(handler: (message: DiscordMessageData) => void): void;
|
|
469
475
|
|
|
470
|
-
/** Register message edit handler
|
|
471
|
-
|
|
476
|
+
/** Register message edit handler. A client that knows the edited
|
|
477
|
+
* message's attachments passes them third; one that passes two arguments
|
|
478
|
+
* leaves a stored row's attachment blocks as they are. */
|
|
479
|
+
onMessageEdit(handler: (messageId: string, newContent: string, attachments?: DiscordAttachment[]) => void): void;
|
|
472
480
|
|
|
473
481
|
/** Register message delete handler */
|
|
474
482
|
onMessageDelete(handler: (messageId: string) => void): void;
|
|
@@ -26,6 +26,7 @@ import type { EventResponse, ProcessState } from '../../types/module.js';
|
|
|
26
26
|
import type { AgentFramework } from '../../framework.js';
|
|
27
27
|
import { formatZonedDateTime, resolveTimeZone } from '../../timezone.js';
|
|
28
28
|
import { safeSlice } from '../../safe-slice.js';
|
|
29
|
+
import { unknownToolMessage } from '../unknown-tool.js';
|
|
29
30
|
|
|
30
31
|
export interface HealthModuleConfig {
|
|
31
32
|
/** IANA zone for timestamps returned by the snapshot tool. */
|
|
@@ -127,7 +128,7 @@ export class HealthModule implements Module {
|
|
|
127
128
|
|
|
128
129
|
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
129
130
|
if (call.name !== 'snapshot') {
|
|
130
|
-
return { success: false, isError: true, error:
|
|
131
|
+
return { success: false, isError: true, error: unknownToolMessage(this.name, call.name, this.getTools()) };
|
|
131
132
|
}
|
|
132
133
|
const input = (call.input ?? {}) as SnapshotInput;
|
|
133
134
|
const lookback = input.lookback ?? this.config.defaultLookback;
|
|
@@ -156,8 +157,11 @@ export class HealthModule implements Module {
|
|
|
156
157
|
includeSubagents: boolean,
|
|
157
158
|
): Record<string, unknown> {
|
|
158
159
|
if (!this.framework || !this.store) {
|
|
160
|
+
// The host's mistake, named for whoever runs it; the agent reads what
|
|
161
|
+
// it can do about it, which is nothing.
|
|
162
|
+
console.error('[health] snapshot asked for before the host bound the module: call bind(framework, store) before tool dispatch');
|
|
159
163
|
throw new Error(
|
|
160
|
-
'
|
|
164
|
+
"The health snapshot isn't available: this host hasn't finished setting up the health module, so nothing was read. Calling it again won't change that; ask whoever runs this host.",
|
|
161
165
|
);
|
|
162
166
|
}
|
|
163
167
|
|
package/src/modules/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type {
|
|
|
30
30
|
SyncInput,
|
|
31
31
|
} from './workspace/index.js';
|
|
32
32
|
|
|
33
|
-
export { DiscordModule, DiscordJsClient } from './discord/index.js';
|
|
33
|
+
export { DiscordModule, DiscordJsClient, DiscordRefusal } from './discord/index.js';
|
|
34
34
|
export type { DiscordJsClientConfig } from './discord/index.js';
|
|
35
35
|
export type {
|
|
36
36
|
DiscordModuleConfig,
|
|
@@ -181,6 +181,12 @@ export class DiskAgreement {
|
|
|
181
181
|
* place owes none (recordWalk).
|
|
182
182
|
*/
|
|
183
183
|
private unsynced = false;
|
|
184
|
+
/**
|
|
185
|
+
* Whether memory holds a whole state: false until a rebuild first runs to
|
|
186
|
+
* its end. A rebuild fills a new map and swaps it in only at its end, so
|
|
187
|
+
* one that throws partway leaves whatever whole state came before it.
|
|
188
|
+
*/
|
|
189
|
+
private whole = false;
|
|
184
190
|
/** Whether this host observes file modes (not on Windows): where it doesn't, no record it writes claims one. */
|
|
185
191
|
private readonly modesObserved: () => boolean;
|
|
186
192
|
|
|
@@ -206,8 +212,10 @@ export class DiskAgreement {
|
|
|
206
212
|
* recorded (the store may hold it where this process can't read it back),
|
|
207
213
|
* and a decision from stale P can record what reopening contradicts, such
|
|
208
214
|
* as a conflict that stays after the right evidence is back. So until the
|
|
209
|
-
* store is reopened, no evidence is read
|
|
210
|
-
* every pass and tool call
|
|
215
|
+
* store is reopened, no evidence is read (but for rootIdentityForRead's
|
|
216
|
+
* one exception), written or checkpointed, and every pass and tool call
|
|
217
|
+
* that needs it fails. The same holds, the exception included, when the
|
|
218
|
+
* open's rebuild threw: there is no whole evidence to read.
|
|
211
219
|
*/
|
|
212
220
|
ensureReconciled(): void {
|
|
213
221
|
if (this.journal.needsReopen) {
|
|
@@ -216,6 +224,12 @@ export class DiskAgreement {
|
|
|
216
224
|
'an append to its journal failed inside the store, and the evidence may lack what it recorded',
|
|
217
225
|
);
|
|
218
226
|
}
|
|
227
|
+
if (!this.whole) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
'Disk-agreement evidence is unavailable until the store is reopened, as at the next start: ' +
|
|
230
|
+
'reading it back from the store failed',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
219
233
|
if (this.journal.needsReconcile) this.rebuild();
|
|
220
234
|
}
|
|
221
235
|
|
|
@@ -253,16 +267,24 @@ export class DiskAgreement {
|
|
|
253
267
|
const mount = replayed.get(entry.mount);
|
|
254
268
|
if (mount) apply(mount, entry);
|
|
255
269
|
}
|
|
256
|
-
|
|
270
|
+
// Filled apart and swapped in at the end, so an append that throws here
|
|
271
|
+
// leaves memory as it was, whole, rather than with the mounts after it
|
|
272
|
+
// missing. Nothing reaches that today: only an open with a moved mount
|
|
273
|
+
// appends here, the workspace opens each agreement once, and after the
|
|
274
|
+
// open a rebuild finds every mount's record and appends none.
|
|
275
|
+
const rebuilt = new Map<string, MountEvidence>();
|
|
257
276
|
for (const { name, root } of this.configured) {
|
|
258
277
|
const found = replayed.get(name);
|
|
259
278
|
if (found && sameRootPath(found.root, root)) {
|
|
260
|
-
|
|
279
|
+
rebuilt.set(name, found);
|
|
261
280
|
} else {
|
|
262
|
-
|
|
281
|
+
rebuilt.set(name, { root, paths: new Map(), walks: new Map() });
|
|
263
282
|
this.journal.append({ t: 'mount', mount: name, root });
|
|
264
283
|
}
|
|
265
284
|
}
|
|
285
|
+
this.mounts.clear();
|
|
286
|
+
for (const [name, mount] of rebuilt) this.mounts.set(name, mount);
|
|
287
|
+
this.whole = true;
|
|
266
288
|
}
|
|
267
289
|
|
|
268
290
|
/** The identity of the root this mount's evidence was gathered under, once any was. */
|
|
@@ -271,6 +293,25 @@ export class DiskAgreement {
|
|
|
271
293
|
return this.mounts.get(mount)?.identity;
|
|
272
294
|
}
|
|
273
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The identity a disk read checks the mount's root against: the one in
|
|
298
|
+
* memory once memory is whole, in every state of the journal, and an error
|
|
299
|
+
* until then. It is the last identity whose append succeeded, since write()
|
|
300
|
+
* appends before it applies, and no reconcile can change it, since root
|
|
301
|
+
* entries are written without a sync; were one made durable, its failed
|
|
302
|
+
* sync would leave the identity from before it: an old one, which refuses
|
|
303
|
+
* the new root, or none, as for a mount with no recorded identity. This
|
|
304
|
+
* is the one exception to ensureReconciled's "no evidence is read": the
|
|
305
|
+
* layer stops reading so that a decision from stale evidence can't record
|
|
306
|
+
* what reopening contradicts, and a read records nothing. Refusing reads
|
|
307
|
+
* instead would stop every host read of the workspace's disk until a
|
|
308
|
+
* restart.
|
|
309
|
+
*/
|
|
310
|
+
rootIdentityForRead(mount: string): RootIdentity | undefined {
|
|
311
|
+
if (!this.whole) this.ensureReconciled(); // throws: there's no whole state to read
|
|
312
|
+
return this.mounts.get(mount)?.identity;
|
|
313
|
+
}
|
|
314
|
+
|
|
274
315
|
/**
|
|
275
316
|
* Record the root's identity just before the first evidence a mount gains:
|
|
276
317
|
* the caller saw it as the root while gathering that evidence. A no-op once
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* Node has no openat, so a create, mkdir or unlink still resolves its path by
|
|
25
25
|
* name after the parent was checked: a parent replaced concurrently in that
|
|
26
26
|
* window can redirect that one operation, leaving an empty file or directory
|
|
27
|
-
* outside the mount or
|
|
27
|
+
* outside the mount or in a nested mount's directory, or unlinking a
|
|
28
|
+
* same-named file there. Nor is there an
|
|
28
29
|
* unlink bound to an inode: an unlink checks the entry's identity again just
|
|
29
30
|
* before it, and a file replaced at its path after that check is the one
|
|
30
31
|
* unlinked. Content writes stay bound to the descriptor verified before
|
|
@@ -53,7 +54,9 @@ import {
|
|
|
53
54
|
errno,
|
|
54
55
|
checkDir,
|
|
55
56
|
checkRoot,
|
|
57
|
+
leadsInto,
|
|
56
58
|
nameOf,
|
|
59
|
+
nestedAtReal,
|
|
57
60
|
parentOf,
|
|
58
61
|
} from './observe.js';
|
|
59
62
|
|
|
@@ -273,7 +276,7 @@ export async function writeContained(
|
|
|
273
276
|
let stage = 'write the file';
|
|
274
277
|
try {
|
|
275
278
|
const created = touched;
|
|
276
|
-
await checkOpened(view, rootReal, root, parent, target, handle, expect, noFollow, touched);
|
|
279
|
+
await checkOpened(view, rootReal, rel, root, parent, target, handle, expect, noFollow, touched);
|
|
277
280
|
touched = true;
|
|
278
281
|
// The mode the bytes are written at, and the one the file ends with.
|
|
279
282
|
let before: number | undefined;
|
|
@@ -369,14 +372,16 @@ export async function writeContained(
|
|
|
369
372
|
* Before an effect on an opened file: a regular file, and — unless forced —
|
|
370
373
|
* still holding what was decided on. The hash reads the whole file, so it
|
|
371
374
|
* comes first, and the checks of where the file is follow it: canonically
|
|
372
|
-
* inside the mount
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* nothing wrote it while
|
|
375
|
+
* inside the mount and in no nested mount's directory (nestedAtReal), the
|
|
376
|
+
* one this path names (and not a symlink, where the mount follows none and
|
|
377
|
+
* the open couldn't refuse one), with no other names, in the directory still
|
|
378
|
+
* at its path beneath the root still at its own; and nothing wrote it while
|
|
379
|
+
* those ran.
|
|
376
380
|
*/
|
|
377
381
|
async function checkOpened(
|
|
378
382
|
view: MountView,
|
|
379
383
|
rootReal: string,
|
|
384
|
+
rel: string,
|
|
380
385
|
root: CheckedDir,
|
|
381
386
|
parent: CheckedDir,
|
|
382
387
|
target: string,
|
|
@@ -391,6 +396,8 @@ async function checkOpened(
|
|
|
391
396
|
const real = await realpath(target).catch(() => null);
|
|
392
397
|
if (real === null) throw new EffectFailed(CHANGED, touched);
|
|
393
398
|
if (!contained(rootReal, real)) throw new EffectFailed(view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT, touched);
|
|
399
|
+
const holder = nestedAtReal(real, view.nested);
|
|
400
|
+
if (holder) throw new EffectFailed(leadsInto(view.name, rel, holder), touched);
|
|
394
401
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
395
402
|
if (named === null || named.dev !== info.dev || named.ino !== info.ino) throw new EffectFailed(CHANGED, touched);
|
|
396
403
|
if (!view.followSymlinks) {
|
|
@@ -459,7 +466,7 @@ export async function chmodContained(
|
|
|
459
466
|
let touched = false;
|
|
460
467
|
let stage = 'set the file\'s mode';
|
|
461
468
|
try {
|
|
462
|
-
await checkOpened(view, rootReal, root, parent, target, handle, expect, noFollow, false);
|
|
469
|
+
await checkOpened(view, rootReal, rel, root, parent, target, handle, expect, noFollow, false);
|
|
463
470
|
const current = Number((await handle.stat({ bigint: true })).mode & 0o777n);
|
|
464
471
|
if (from !== undefined && current !== from) throw new EffectFailed(MODE_CHANGED, false);
|
|
465
472
|
const narrowing = (current & ~mode & 0o777) !== 0;
|
|
@@ -525,6 +532,10 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
|
|
|
525
532
|
if (!view.followSymlinks) throw new EffectFailed(NOT_FOLLOWED, false);
|
|
526
533
|
const real = await realpath(target).catch(() => null);
|
|
527
534
|
if (real === null || !contained(rootReal, real)) throw new EffectFailed('a symlink that leaves the mount', false);
|
|
535
|
+
// One leading into a nested mount's directory is that mount's: the link
|
|
536
|
+
// and the file it leads to are left as they are.
|
|
537
|
+
const holder = nestedAtReal(real, view.nested);
|
|
538
|
+
if (holder) throw new EffectFailed(leadsInto(view.name, rel, holder), false);
|
|
528
539
|
const through = await stat(real, { bigint: true }).catch(() => null);
|
|
529
540
|
if (through === null) throw new EffectFailed(CHANGED, false);
|
|
530
541
|
// A followed link stands for the file it leads to, so it's removed only
|