@myagentroam/node 0.11.21 → 0.11.22
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/capabilities.js +1 -0
- package/dist/connector.js +33 -0
- package/dist/runner/mar-agent/compact-extensions.js +26 -1
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +10 -0
- package/dist/runner/mar-agent/image-generation-extensions.js +1 -1
- package/dist/runner/mar-agent/managed-run-controller.js +1 -0
- package/dist/runner/mar-agent/runtime-model-factory.js +11 -10
- package/dist/runner/mar-agent/session-runtime.js +6 -0
- package/dist/runner/mar-agent-runner.js +17 -1
- package/dist/service/model-resource-publication-service.js +148 -0
- package/dist/service/run-workbench-service.js +11 -1
- package/dist/service/runtime-credential-control-service.js +8 -9
- package/dist/service/workspace-watch-controller.js +7 -1
- package/dist/service/workspace-watch-service.js +8 -1
- package/dist/service/workspace-watch-snapshot-service.js +26 -3
- package/dist/util/node-operation-parsers.js +9 -5
- package/package.json +3 -3
package/dist/capabilities.js
CHANGED
|
@@ -14,6 +14,7 @@ const runtimeCredentialCapability = () => ({
|
|
|
14
14
|
responsesOptionalParameters: true,
|
|
15
15
|
responsesHttpContinuation: true,
|
|
16
16
|
conversationModels: true,
|
|
17
|
+
resourceInput: true,
|
|
17
18
|
httpVersions: ['1.1', '2']
|
|
18
19
|
});
|
|
19
20
|
const workspaceDirectTransferCapability = () => ({
|
package/dist/connector.js
CHANGED
|
@@ -83,6 +83,7 @@ import { SessionCommandService } from './service/session-command-service.js';
|
|
|
83
83
|
import { SessionPresentationService, sessionGroup } from './service/session-presentation-service.js';
|
|
84
84
|
import { isPlainRecord, isRunnerImageAttachment, parseRunnerImageAttachments } from './util/node-operation-parsers.js';
|
|
85
85
|
import { WebToolService } from './service/web-tool-service.js';
|
|
86
|
+
import { ModelResourcePublicationService } from './service/model-resource-publication-service.js';
|
|
86
87
|
export { nodeConnectEndpoint, validatedCapabilities } from './util/node-operation-parsers.js';
|
|
87
88
|
import { NodeControlChannel } from './service/node-control-channel.js';
|
|
88
89
|
import { NodeControlMessageService } from './service/node-control-message-service.js';
|
|
@@ -241,6 +242,7 @@ export class NodeConnector {
|
|
|
241
242
|
sessionCatalogService;
|
|
242
243
|
sessionCommandService;
|
|
243
244
|
runAttachmentService;
|
|
245
|
+
modelResourcePublicationService;
|
|
244
246
|
terminalChannel;
|
|
245
247
|
codexClient;
|
|
246
248
|
codexRunController;
|
|
@@ -448,6 +450,11 @@ export class NodeConnector {
|
|
|
448
450
|
capabilities: () => this.capabilities,
|
|
449
451
|
probe: this.conversationHostingProbe,
|
|
450
452
|
runtime: {
|
|
453
|
+
publishModelResource: (image, context) => this.modelResourcePublicationService.publish({
|
|
454
|
+
conversationId: context.conversationId,
|
|
455
|
+
sessionId: context.sessionId,
|
|
456
|
+
homeDirectory: context.homeDirectory
|
|
457
|
+
}, image),
|
|
451
458
|
webSearch: (input, signal) => this.webToolService.webSearch(input, signal),
|
|
452
459
|
webFetch: (input, signal) => this.webToolService.webFetch(input, signal, false),
|
|
453
460
|
modelFactory: this.marAgentRuntimeModels,
|
|
@@ -502,6 +509,20 @@ export class NodeConnector {
|
|
|
502
509
|
this.fakeScript = options.fakeScript;
|
|
503
510
|
this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoots(session), (sessionId) => this.runtime.activeSessionRun(sessionId) !== undefined ||
|
|
504
511
|
this.runtime.sessionQueue(sessionId).itemIds.length > 0, () => this.attachmentTemporaryRoot());
|
|
512
|
+
this.modelResourcePublicationService = new ModelResourcePublicationService({
|
|
513
|
+
directory: () => resolve(nodeDataDirectory(this.config, this.configPath), 'model-resources'),
|
|
514
|
+
nodeId: () => this.config?.nodeId,
|
|
515
|
+
origin: () => {
|
|
516
|
+
if (!this.config)
|
|
517
|
+
return undefined;
|
|
518
|
+
const url = new URL(this.config.serverUrl);
|
|
519
|
+
if (url.protocol === 'ws:')
|
|
520
|
+
url.protocol = 'http:';
|
|
521
|
+
if (url.protocol === 'wss:')
|
|
522
|
+
url.protocol = 'https:';
|
|
523
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.origin : undefined;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
505
526
|
this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory, options.codexRunClientFactory);
|
|
506
527
|
this.codexRunController = new CodexManagedRunController(this.codexClient, {
|
|
507
528
|
available: () => this.capabilities.codex.available,
|
|
@@ -593,6 +614,16 @@ export class NodeConnector {
|
|
|
593
614
|
}
|
|
594
615
|
}, {
|
|
595
616
|
modelFactory: this.marAgentRuntimeModels,
|
|
617
|
+
publishModelResource: (image, context) => {
|
|
618
|
+
const session = this.runtime.getAgentSession(context.ownerSessionId);
|
|
619
|
+
if (session === undefined)
|
|
620
|
+
throw new Error('RUN_SESSION_NOT_FOUND');
|
|
621
|
+
return this.modelResourcePublicationService.publish({
|
|
622
|
+
workspaceId: session.workspaceId,
|
|
623
|
+
sessionId: context.sessionId,
|
|
624
|
+
homeDirectory: context.homeDirectory
|
|
625
|
+
}, image);
|
|
626
|
+
},
|
|
596
627
|
retainEnvironment: (environment) => this.gitExecution().retain(environment),
|
|
597
628
|
computerUse: this.computerUseProvider
|
|
598
629
|
});
|
|
@@ -723,6 +754,7 @@ export class NodeConnector {
|
|
|
723
754
|
readImage: async (runId, selector) => typeof selector === 'number'
|
|
724
755
|
? this.runAttachmentService.read(runId, selector)
|
|
725
756
|
: this.runAttachmentService.readNative(runId, selector),
|
|
757
|
+
readModelResource: (input) => this.modelResourcePublicationService.read(input),
|
|
726
758
|
respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
|
|
727
759
|
pauseQueue: (sessionId) => this.workspaceQueueWorkbenchService.pauseSessionQueue(sessionId),
|
|
728
760
|
runInterrupted: (sessionId) => {
|
|
@@ -1681,6 +1713,7 @@ export class NodeConnector {
|
|
|
1681
1713
|
this.runEventService.clear();
|
|
1682
1714
|
this.sessionContextService.dispose();
|
|
1683
1715
|
this.runAttachmentService.dispose();
|
|
1716
|
+
this.modelResourcePublicationService.dispose();
|
|
1684
1717
|
const marAgentDrain = this.marAgentRunController.dispose();
|
|
1685
1718
|
const conversationHostingDrain = this.conversationHosting.dispose();
|
|
1686
1719
|
const uploadDrain = this.workspaceUploadService.shutdown();
|
|
@@ -70,18 +70,21 @@ export async function generateMarAgentCompact(context, selected, signal) {
|
|
|
70
70
|
const budget = Math.floor(window * 0.9) - overhead;
|
|
71
71
|
let modelRequests = 0;
|
|
72
72
|
let inputTokensEstimateAfter = overhead;
|
|
73
|
+
let failure = {};
|
|
73
74
|
const report = async (state, reason, errorCode) => {
|
|
74
75
|
try {
|
|
75
76
|
await context.reportDiagnostic?.({
|
|
76
77
|
modelId: selected.model.id,
|
|
77
78
|
providerModelId: selected.model.modelId,
|
|
78
79
|
reasoningEffort: selected.reasoningEffort,
|
|
80
|
+
trigger: context.reason,
|
|
79
81
|
state,
|
|
80
82
|
inputTokensEstimateBefore: countModelTokens(full) + overhead,
|
|
81
83
|
inputTokensEstimateAfter,
|
|
82
84
|
modelRequests,
|
|
83
85
|
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
84
86
|
...(reason === undefined ? {} : { reason }),
|
|
87
|
+
...failure,
|
|
85
88
|
...(errorCode === undefined ? {} : { errorCode })
|
|
86
89
|
});
|
|
87
90
|
}
|
|
@@ -114,9 +117,20 @@ export async function generateMarAgentCompact(context, selected, signal) {
|
|
|
114
117
|
outputLimit: Math.min(selected.model.maxOutputTokens, context.summaryTokenBudget),
|
|
115
118
|
onAttemptDiagnostic: async (diagnostic) => {
|
|
116
119
|
if (diagnostic.state === 'STARTED') {
|
|
120
|
+
failure = {};
|
|
117
121
|
modelRequests++;
|
|
118
122
|
await report('STARTED');
|
|
119
123
|
}
|
|
124
|
+
else if (diagnostic.state === 'FAILED') {
|
|
125
|
+
failure = {
|
|
126
|
+
...(diagnostic.errorCode === undefined ? {} : { errorCode: diagnostic.errorCode }),
|
|
127
|
+
...(diagnostic.causeCode === undefined ? {} : { causeCode: diagnostic.causeCode }),
|
|
128
|
+
...(diagnostic.reason === undefined ? {} : { errorReason: diagnostic.reason }),
|
|
129
|
+
...(diagnostic.retryable === undefined ? {} : { retryable: diagnostic.retryable }),
|
|
130
|
+
transport: diagnostic.transport
|
|
131
|
+
};
|
|
132
|
+
await report('FAILED', 'MODEL_FAILED');
|
|
133
|
+
}
|
|
120
134
|
},
|
|
121
135
|
signal
|
|
122
136
|
});
|
|
@@ -127,7 +141,18 @@ export async function generateMarAgentCompact(context, selected, signal) {
|
|
|
127
141
|
}
|
|
128
142
|
catch (error) {
|
|
129
143
|
signal.throwIfAborted();
|
|
130
|
-
|
|
144
|
+
if (error instanceof MarAgentError) {
|
|
145
|
+
const status = error.details?.status;
|
|
146
|
+
failure = {
|
|
147
|
+
...failure,
|
|
148
|
+
retryable: error.retryable,
|
|
149
|
+
...(typeof status === 'number' ? { httpStatus: status } : {}),
|
|
150
|
+
...(typeof error.details?.reason === 'string' ? { errorReason: error.details.reason } : {})
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
await report('FAILED', 'MODEL_FAILED', error instanceof MarAgentError
|
|
154
|
+
? error.code
|
|
155
|
+
: (failure.errorCode ?? 'MAR_AGENT_COMPACT_MODEL_FAILED'));
|
|
131
156
|
return undefined;
|
|
132
157
|
}
|
|
133
158
|
}
|
|
@@ -17,6 +17,7 @@ export class MarAgentHostedConversationRuntime {
|
|
|
17
17
|
#createAgent;
|
|
18
18
|
#generateTitle;
|
|
19
19
|
#createHost;
|
|
20
|
+
#publishModelResource;
|
|
20
21
|
#openCatalog;
|
|
21
22
|
#now;
|
|
22
23
|
#onEvent;
|
|
@@ -26,6 +27,7 @@ export class MarAgentHostedConversationRuntime {
|
|
|
26
27
|
#modelFactory;
|
|
27
28
|
#registerNativeImages;
|
|
28
29
|
constructor(options = {}) {
|
|
30
|
+
this.#publishModelResource = options.publishModelResource;
|
|
29
31
|
this.#createAgent = options.createAgent ?? createMarAgent;
|
|
30
32
|
this.#generateTitle = options.generateTitle ?? generateSessionTitle;
|
|
31
33
|
this.#createHost = options.createHost ?? ((input) => new LocalAgentHost(input));
|
|
@@ -341,6 +343,14 @@ export class MarAgentHostedConversationRuntime {
|
|
|
341
343
|
...(this.#webFetch === undefined ? {} : { webFetch: this.#webFetch })
|
|
342
344
|
}));
|
|
343
345
|
host = this.#createHost({
|
|
346
|
+
...(this.#publishModelResource === undefined
|
|
347
|
+
? {}
|
|
348
|
+
: {
|
|
349
|
+
publishModelResource: (image, context) => this.#publishModelResource(image, {
|
|
350
|
+
...context,
|
|
351
|
+
conversationId: input.conversationId
|
|
352
|
+
})
|
|
353
|
+
}),
|
|
344
354
|
...('onModelUsage' in input && input.onModelUsage !== undefined
|
|
345
355
|
? { modelUsage: input.onModelUsage }
|
|
346
356
|
: {}),
|
|
@@ -46,7 +46,7 @@ export function createImageGenerationExtensions(input) {
|
|
|
46
46
|
.host()
|
|
47
47
|
.executeTool({ name: 'view_image', arguments: { path } }, context);
|
|
48
48
|
const image = output.images?.length === 1 ? output.images[0] : undefined;
|
|
49
|
-
if (image === undefined)
|
|
49
|
+
if (image === undefined || image.dataBase64 === undefined)
|
|
50
50
|
throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_INVALID', 'Referenced image result is invalid.');
|
|
51
51
|
const data = Buffer.from(image.dataBase64, 'base64');
|
|
52
52
|
if (data.byteLength === 0)
|
|
@@ -52,6 +52,7 @@ export class MarAgentManagedRunController {
|
|
|
52
52
|
}),
|
|
53
53
|
customTitle: (sessionId) => this.host.customTitle?.(sessionId) ?? undefined,
|
|
54
54
|
modelUsage: (runId, usage) => this.host.emit(runId, 'run.usage', normalizeMarAgentUsage(usage)),
|
|
55
|
+
publishModelResource: options.publishModelResource,
|
|
55
56
|
question: (runId, request, signal) => this.requestQuestion(runId, request, signal),
|
|
56
57
|
...(this.host.createApproval && this.host.publishApproval
|
|
57
58
|
? {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SecretValue } from '@myagentroam/agent';
|
|
1
|
+
import { MarAgentError, SecretValue } from '@myagentroam/agent';
|
|
2
2
|
import { marAgentRuntimeModelConfigurationSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
|
|
3
3
|
export function sdkModelConfigurations(models) {
|
|
4
4
|
return models.map(({ imageGeneration: _imageGeneration, ...model }) => model);
|
|
@@ -72,6 +72,7 @@ export class MarAgentRuntimeModelFactory {
|
|
|
72
72
|
? { fastMode: true }
|
|
73
73
|
: {}),
|
|
74
74
|
hostedWebSearch: configuration.hostedWebSearch,
|
|
75
|
+
useResourceUrl: configuration.useResourceUrl,
|
|
75
76
|
...(imageGeneration === undefined ? {} : { imageGeneration }),
|
|
76
77
|
...(configuration.responsesTransport === undefined
|
|
77
78
|
? {}
|
|
@@ -94,11 +95,11 @@ export class MarAgentRuntimeModelFactory {
|
|
|
94
95
|
apiKey: new SecretValue(credential.value)
|
|
95
96
|
};
|
|
96
97
|
if (credential?.type !== 'RUNTIME_TICKET')
|
|
97
|
-
throw new
|
|
98
|
+
throw new MarAgentError('MAR_AGENT_RUNTIME_MODEL_CREDENTIAL_INVALID', 'The runtime model credential is invalid.');
|
|
98
99
|
if (context.runtimeScopeId === undefined ||
|
|
99
100
|
context.runtimeScopeId !== credential.ticket.runtimeScopeId ||
|
|
100
101
|
!credential.ticket.allowedModelIds.includes(configuration.id))
|
|
101
|
-
throw new
|
|
102
|
+
throw new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_SCOPE_INVALID', 'The runtime credential does not authorize this execution or model.');
|
|
102
103
|
const provider = runtimeProvider(credential, configuration.id, context, tickets, providers, this.control, this.now);
|
|
103
104
|
return {
|
|
104
105
|
...model,
|
|
@@ -140,7 +141,7 @@ function runtimeProvider(credential, consumerId, context, tickets, providers, co
|
|
|
140
141
|
if (context.runtimeScopeId === undefined ||
|
|
141
142
|
context.runtimeScopeId !== credential.ticket.runtimeScopeId ||
|
|
142
143
|
!credential.ticket.allowedModelIds.includes(consumerId))
|
|
143
|
-
throw new
|
|
144
|
+
throw new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_SCOPE_INVALID', 'The runtime credential does not authorize this execution or model.');
|
|
144
145
|
const key = `${credential.credentialId}\0${credential.ticket.runtimeScopeId}\0${credential.ticket.purpose ?? 'MODEL'}`;
|
|
145
146
|
let state = tickets.get(key);
|
|
146
147
|
if (state === undefined) {
|
|
@@ -152,7 +153,7 @@ function runtimeProvider(credential, consumerId, context, tickets, providers, co
|
|
|
152
153
|
tickets.set(key, state);
|
|
153
154
|
}
|
|
154
155
|
else if (!sameRuntimeCredentialTicket(state.ticket, credential.ticket))
|
|
155
|
-
throw new
|
|
156
|
+
throw new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_SCOPE_INVALID', 'The runtime credential does not authorize this execution or model.');
|
|
156
157
|
const provider = new ServerRuntimeCredentialProvider(control, state, consumerId, context.runtimeScopeId, context.executionId, now);
|
|
157
158
|
providers.push(provider);
|
|
158
159
|
return provider;
|
|
@@ -178,7 +179,7 @@ class ServerRuntimeCredentialProvider {
|
|
|
178
179
|
}
|
|
179
180
|
async acquire(reason) {
|
|
180
181
|
if (this.#closed || this.state.released)
|
|
181
|
-
throw new
|
|
182
|
+
throw new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_PROVIDER_CLOSED', 'The runtime credential provider is closed.');
|
|
182
183
|
if (isReusableCredential(this.state.credential, this.now())) {
|
|
183
184
|
if (isWithinRefreshWindow(this.state.ticket.expiresAt, this.now()))
|
|
184
185
|
this.refreshAutomatically();
|
|
@@ -263,9 +264,9 @@ class ServerRuntimeCredentialProvider {
|
|
|
263
264
|
reason
|
|
264
265
|
}, signal);
|
|
265
266
|
if (this.state.released)
|
|
266
|
-
throw new
|
|
267
|
+
throw new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_PROVIDER_CLOSED', 'The runtime credential provider is closed.');
|
|
267
268
|
if ('error' in response)
|
|
268
|
-
throw new
|
|
269
|
+
throw new MarAgentError(response.error.code, response.error.message);
|
|
269
270
|
this.state.ticket = response.nextTicket;
|
|
270
271
|
const credential = {
|
|
271
272
|
revision: response.credential.revision,
|
|
@@ -332,11 +333,11 @@ function clearRefreshTimer(state) {
|
|
|
332
333
|
}
|
|
333
334
|
function waitForCredential(pending, signal) {
|
|
334
335
|
if (signal.aborted)
|
|
335
|
-
return Promise.reject(new
|
|
336
|
+
return Promise.reject(new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_PROVIDER_CLOSED', 'The runtime credential provider is closed.'));
|
|
336
337
|
return new Promise((resolve, reject) => {
|
|
337
338
|
const abort = () => {
|
|
338
339
|
cleanup();
|
|
339
|
-
reject(new
|
|
340
|
+
reject(new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_PROVIDER_CLOSED', 'The runtime credential provider is closed.'));
|
|
340
341
|
};
|
|
341
342
|
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
342
343
|
signal.addEventListener('abort', abort, { once: true });
|
|
@@ -30,6 +30,12 @@ export async function createSessionRuntime(input, sessionKey, fingerprint, hostC
|
|
|
30
30
|
releaseEnvironment = dependencies.retainEnvironment?.(input.secretEnvironment ?? {});
|
|
31
31
|
const mcp = mcpLease.pool;
|
|
32
32
|
const localHost = new LocalAgentHost({
|
|
33
|
+
publishModelResource: dependencies.publishModelResource === undefined
|
|
34
|
+
? undefined
|
|
35
|
+
: (image, context) => dependencies.publishModelResource(image, {
|
|
36
|
+
...context,
|
|
37
|
+
ownerSessionId: input.sessionId
|
|
38
|
+
}),
|
|
33
39
|
modelUsage: (usage) => {
|
|
34
40
|
const runId = currentRunId();
|
|
35
41
|
if (runId !== undefined)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { marAgentNodeModelSummarySchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
|
|
1
|
+
import { marAgentNodeModelSummarySchema, parseComposerTags, resolveMarAgentDefaultReasoningEffort, serializeComposerTag } from '@myagentroam/protocol';
|
|
2
2
|
import { nodeLog } from '../operational.js';
|
|
3
3
|
import { runnerErrorProjection } from '../util/runner-error.js';
|
|
4
4
|
import { MarAgentError, openMarAgentSessionCatalog } from '@myagentroam/agent';
|
|
@@ -11,6 +11,22 @@ import { captureMarAgentExecutionInput } from './mar-agent/execution-input.js';
|
|
|
11
11
|
export class MarAgentRunner extends AbstractRunner {
|
|
12
12
|
homeDirectory;
|
|
13
13
|
name = 'mar-agent';
|
|
14
|
+
prepareMessageInput(input, collaborationMode) {
|
|
15
|
+
const prepared = super.prepareMessageInput(input, collaborationMode);
|
|
16
|
+
return parseComposerTags(prepared)
|
|
17
|
+
.map((part) => {
|
|
18
|
+
if (part.kind === 'text')
|
|
19
|
+
return part.text;
|
|
20
|
+
if (part.kind === 'mar_attachments' &&
|
|
21
|
+
part.payload.files.some((file) => file.mime.startsWith('image/')))
|
|
22
|
+
return serializeComposerTag('mar_attachments', {
|
|
23
|
+
...part.payload,
|
|
24
|
+
instruction: 'Images are included. Use view_image only to revisit them.'
|
|
25
|
+
});
|
|
26
|
+
return part.raw;
|
|
27
|
+
})
|
|
28
|
+
.join('');
|
|
29
|
+
}
|
|
14
30
|
prepareExecutionContext(input) {
|
|
15
31
|
return { runner: this.name, runnerInput: captureMarAgentExecutionInput(input) };
|
|
16
32
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { MarAgentError, openMarAgentSessionCatalog } from '@myagentroam/agent';
|
|
5
|
+
import { modelResourceReadInputSchema } from '@myagentroam/protocol';
|
|
6
|
+
const TTL_MS = 24 * 60 * 60_000;
|
|
7
|
+
/** Only capability metadata lives here. The SDK owns the sole immutable image file. */
|
|
8
|
+
export class ModelResourcePublicationService {
|
|
9
|
+
options;
|
|
10
|
+
#pending = new Map();
|
|
11
|
+
#key;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
async publish(source, resource) {
|
|
16
|
+
const origin = this.options.origin();
|
|
17
|
+
const nodeId = this.options.nodeId();
|
|
18
|
+
if (origin === undefined || nodeId === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const id = this.identity(source, resource.resourceId);
|
|
21
|
+
const current = this.#pending.get(id);
|
|
22
|
+
if (current !== undefined)
|
|
23
|
+
return current;
|
|
24
|
+
const creation = this.create(id, source, resource.resourceId, origin, nodeId);
|
|
25
|
+
this.#pending.set(id, creation);
|
|
26
|
+
try {
|
|
27
|
+
return await creation;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (this.#pending.get(id) === creation)
|
|
31
|
+
this.#pending.delete(id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async create(id, source, resourceId, origin, nodeId) {
|
|
35
|
+
const key = await this.signingKey();
|
|
36
|
+
try {
|
|
37
|
+
const record = JSON.parse(await readFile(this.path(id), 'utf8'));
|
|
38
|
+
if (record.version !== 2 || this.identity(record.source, record.resourceId) !== id)
|
|
39
|
+
throw new Error('MODEL_RESOURCE_INVALID');
|
|
40
|
+
return { url: record.url, urlExpiresAt: record.expiresAt };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code !== 'ENOENT')
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const expiresAt = this.now() + TTL_MS;
|
|
47
|
+
const signature = this.signature(key, nodeId, source, resourceId, expiresAt);
|
|
48
|
+
const scope = 'workspaceId' in source
|
|
49
|
+
? `workspaces/${encodeURIComponent(source.workspaceId)}`
|
|
50
|
+
: `conversations/${encodeURIComponent(source.conversationId)}`;
|
|
51
|
+
const url = `${origin}/api/nodes/${encodeURIComponent(nodeId)}/${scope}/sessions/${encodeURIComponent(source.sessionId)}/resources/${resourceId}?expiresAt=${expiresAt}&signature=${signature}`;
|
|
52
|
+
const record = { version: 2, source, resourceId, expiresAt, url };
|
|
53
|
+
const temporary = join(this.options.directory(), `.${id}.${randomUUID()}.tmp`);
|
|
54
|
+
try {
|
|
55
|
+
await writeFile(temporary, JSON.stringify(record), { mode: 0o600, flag: 'wx', flush: true });
|
|
56
|
+
await rename(temporary, this.path(id));
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await rm(temporary, { force: true });
|
|
60
|
+
}
|
|
61
|
+
return { url, urlExpiresAt: expiresAt };
|
|
62
|
+
}
|
|
63
|
+
async read(input) {
|
|
64
|
+
if (!modelResourceReadInputSchema.safeParse(input).success)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (input.expiresAt <= this.now())
|
|
67
|
+
return undefined;
|
|
68
|
+
const nodeId = this.options.nodeId();
|
|
69
|
+
if (nodeId === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const scope = 'workspaceId' in input
|
|
72
|
+
? { workspaceId: input.workspaceId }
|
|
73
|
+
: { conversationId: input.conversationId };
|
|
74
|
+
const source = { ...scope, sessionId: input.sessionId };
|
|
75
|
+
const expected = this.signature(await this.signingKey(), nodeId, source, input.resourceId, input.expiresAt);
|
|
76
|
+
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(input.signature)))
|
|
77
|
+
return undefined;
|
|
78
|
+
let record;
|
|
79
|
+
try {
|
|
80
|
+
record = JSON.parse(await readFile(this.path(this.identity(source, input.resourceId)), 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code === 'ENOENT')
|
|
84
|
+
return undefined;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
if (record.version !== 2 ||
|
|
88
|
+
record.expiresAt !== input.expiresAt ||
|
|
89
|
+
this.identity(record.source, record.resourceId) !== this.identity(source, input.resourceId))
|
|
90
|
+
return undefined;
|
|
91
|
+
const catalog = await openMarAgentSessionCatalog({
|
|
92
|
+
homeDirectory: record.source.homeDirectory
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
return await catalog.readResource(record.source.sessionId, record.resourceId);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof MarAgentError && error.code === 'MAR_AGENT_SESSION_NOT_FOUND')
|
|
99
|
+
return undefined;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await catalog.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
dispose() {
|
|
107
|
+
this.#pending.clear();
|
|
108
|
+
}
|
|
109
|
+
now() {
|
|
110
|
+
return this.options.now?.() ?? Date.now();
|
|
111
|
+
}
|
|
112
|
+
path(id) {
|
|
113
|
+
return join(this.options.directory(), `${id}.json`);
|
|
114
|
+
}
|
|
115
|
+
identity(source, resourceId) {
|
|
116
|
+
return createHash('sha256')
|
|
117
|
+
.update(JSON.stringify([
|
|
118
|
+
'workspaceId' in source ? 'workspace' : 'conversation',
|
|
119
|
+
'workspaceId' in source ? source.workspaceId : source.conversationId,
|
|
120
|
+
source.sessionId,
|
|
121
|
+
resourceId
|
|
122
|
+
]))
|
|
123
|
+
.digest('hex');
|
|
124
|
+
}
|
|
125
|
+
signature(key, nodeId, source, resourceId, expiresAt) {
|
|
126
|
+
return createHmac('sha256', key)
|
|
127
|
+
.update(JSON.stringify([nodeId, this.identity(source, resourceId), expiresAt]))
|
|
128
|
+
.digest('base64url');
|
|
129
|
+
}
|
|
130
|
+
signingKey() {
|
|
131
|
+
return (this.#key ??= (async () => {
|
|
132
|
+
const directory = this.options.directory();
|
|
133
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
134
|
+
const path = join(directory, 'signing-key');
|
|
135
|
+
try {
|
|
136
|
+
await writeFile(path, randomBytes(32), { mode: 0o600, flag: 'wx', flush: true });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.code !== 'EEXIST')
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const key = await readFile(path);
|
|
143
|
+
if (key.length !== 32)
|
|
144
|
+
throw new Error('MODEL_RESOURCE_SIGNING_KEY_INVALID');
|
|
145
|
+
return key;
|
|
146
|
+
})());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runImageReadInputSchema } from '@myagentroam/protocol';
|
|
1
|
+
import { modelResourceReadInputSchema, runImageReadInputSchema } from '@myagentroam/protocol';
|
|
2
2
|
import { nodeOperationData } from '../util/node-operation-data.js';
|
|
3
3
|
import { isTerminalRunStatus } from '../util/node-operation-parsers.js';
|
|
4
4
|
import { isPlainRecord } from '../util/node-operation-parsers.js';
|
|
@@ -13,6 +13,7 @@ export class RunWorkbenchService {
|
|
|
13
13
|
return {
|
|
14
14
|
'run.get': (data) => this.get(record(data)),
|
|
15
15
|
'run.image.read': (data) => this.image(record(data)),
|
|
16
|
+
'model.resource.read': (data) => this.modelResource(record(data)),
|
|
16
17
|
'run.user-input.respond': (data) => this.respond(record(data)),
|
|
17
18
|
'session.interrupt': (data) => this.interruptSession(record(data)),
|
|
18
19
|
'run.cancel': (data) => this.cancelOrInterrupt('run.cancel', record(data)),
|
|
@@ -20,6 +21,15 @@ export class RunWorkbenchService {
|
|
|
20
21
|
'run.approval.decide': (data) => this.decideApproval(record(data))
|
|
21
22
|
};
|
|
22
23
|
}
|
|
24
|
+
async modelResource(input) {
|
|
25
|
+
const parsed = modelResourceReadInputSchema.safeParse(nodeOperationData(input));
|
|
26
|
+
if (!parsed.success)
|
|
27
|
+
throw new Error('RUN_IMAGE_INVALID');
|
|
28
|
+
const image = await this.options.readModelResource?.(parsed.data);
|
|
29
|
+
if (image === undefined)
|
|
30
|
+
throw new Error('RUN_IMAGE_NOT_FOUND');
|
|
31
|
+
return { image };
|
|
32
|
+
}
|
|
23
33
|
get(input) {
|
|
24
34
|
return { run: this.requireRun(input.runId) };
|
|
25
35
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MarAgentError } from '@myagentroam/agent';
|
|
1
2
|
import { createEnvelope, runtimeCredentialResolveRequestSchema, runtimeCredentialResolveResponseSchema } from '@myagentroam/protocol';
|
|
2
3
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
3
4
|
/** Correlates the closed runtime-credential exchange on the authenticated control connection. */
|
|
@@ -32,7 +33,7 @@ export class RuntimeCredentialControlService {
|
|
|
32
33
|
return true;
|
|
33
34
|
const parsed = runtimeCredentialResolveResponseSchema.safeParse(envelope.payload);
|
|
34
35
|
if (!parsed.success) {
|
|
35
|
-
pending.reject(new
|
|
36
|
+
pending.reject(new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_RESPONSE_INVALID', 'The runtime credential response is invalid.'));
|
|
36
37
|
return true;
|
|
37
38
|
}
|
|
38
39
|
pending.resolve(parsed.data);
|
|
@@ -40,9 +41,9 @@ export class RuntimeCredentialControlService {
|
|
|
40
41
|
}
|
|
41
42
|
resolve(request, signal) {
|
|
42
43
|
if (this.#closed)
|
|
43
|
-
return Promise.reject(new
|
|
44
|
+
return Promise.reject(new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_CONTROL_CLOSED', 'The runtime credential control service is closed.'));
|
|
44
45
|
if (!this.#connected)
|
|
45
|
-
return Promise.reject(new
|
|
46
|
+
return Promise.reject(new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_CONNECTION_INVALID', 'The runtime credential control connection is unavailable.'));
|
|
46
47
|
const parsed = runtimeCredentialResolveRequestSchema.parse(request);
|
|
47
48
|
const envelope = createEnvelope('credential.resolve', parsed);
|
|
48
49
|
const generation = this.#generation;
|
|
@@ -56,8 +57,8 @@ export class RuntimeCredentialControlService {
|
|
|
56
57
|
pending.cleanup();
|
|
57
58
|
action(value);
|
|
58
59
|
};
|
|
59
|
-
const abort = () => settle((error) => reject(error), new
|
|
60
|
-
const timer = setTimeout(() => settle((error) => reject(error), new
|
|
60
|
+
const abort = () => settle((error) => reject(error), new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_PROVIDER_CLOSED', 'The runtime credential provider is closed.'));
|
|
61
|
+
const timer = setTimeout(() => settle((error) => reject(error), new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_REQUEST_TIMEOUT', 'The runtime credential request timed out.')), this.#timeoutMs);
|
|
61
62
|
timer.unref();
|
|
62
63
|
this.#pending.set(envelope.id, {
|
|
63
64
|
generation,
|
|
@@ -74,9 +75,7 @@ export class RuntimeCredentialControlService {
|
|
|
74
75
|
this.options.send(envelope);
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
|
-
settle((cause) => reject(cause), error
|
|
78
|
-
? error
|
|
79
|
-
: new Error('MAR_AGENT_RUNTIME_CREDENTIAL_REQUEST_SEND_FAILED'));
|
|
78
|
+
settle((cause) => reject(cause), new MarAgentError('MAR_AGENT_RUNTIME_CREDENTIAL_REQUEST_SEND_FAILED', 'The runtime credential request could not be sent.', { cause: error }));
|
|
80
79
|
}
|
|
81
80
|
});
|
|
82
81
|
}
|
|
@@ -90,6 +89,6 @@ export class RuntimeCredentialControlService {
|
|
|
90
89
|
}
|
|
91
90
|
#rejectPending(code) {
|
|
92
91
|
for (const pending of [...this.#pending.values()])
|
|
93
|
-
pending.reject(new
|
|
92
|
+
pending.reject(new MarAgentError(code, 'The runtime credential control connection is no longer valid.'));
|
|
94
93
|
}
|
|
95
94
|
}
|
|
@@ -22,6 +22,12 @@ export class WorkspaceWatchController {
|
|
|
22
22
|
}
|
|
23
23
|
const topics = this.options.watch.parseTopics(input);
|
|
24
24
|
const existing = this.options.watch.existing(workspace.id, key);
|
|
25
|
+
const groupOnly = existing !== undefined &&
|
|
26
|
+
existing.topics.sessions === topics.sessions &&
|
|
27
|
+
existing.topics.sessionGroup !== topics.sessionGroup &&
|
|
28
|
+
existing.topics.changes === topics.changes &&
|
|
29
|
+
JSON.stringify(existing.topics.files) === JSON.stringify(topics.files) &&
|
|
30
|
+
(existing.prepareFiles === true) === (input.prepareFiles === true);
|
|
25
31
|
if (existing !== undefined &&
|
|
26
32
|
existing.watchId === input.watchId &&
|
|
27
33
|
JSON.stringify(existing.topics) === JSON.stringify(topics) &&
|
|
@@ -30,7 +36,7 @@ export class WorkspaceWatchController {
|
|
|
30
36
|
return this.accepted(existing);
|
|
31
37
|
}
|
|
32
38
|
const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), input.workbenchConnectionId, this.options.nodeGeneration(), topics, input.prepareFiles === true);
|
|
33
|
-
void this.options.snapshots.initial(workspace, watch);
|
|
39
|
+
void this.options.snapshots.initial(workspace, watch, groupOnly ? existing : undefined);
|
|
34
40
|
if (watch.prepareFiles)
|
|
35
41
|
this.options.snapshots.prewarmFiles(workspace, watch);
|
|
36
42
|
return this.accepted(watch);
|
|
@@ -59,8 +59,9 @@ export class WorkspaceWatchService {
|
|
|
59
59
|
create(workspaceId, watchId, sessionHash, workbenchConnectionId, nodeGeneration, topics, prepareFiles = false) {
|
|
60
60
|
const key = `${sessionHash}:${workbenchConnectionId}`;
|
|
61
61
|
for (const [activeWorkspaceId, watches] of this.state.watches)
|
|
62
|
-
if (watches.has(key))
|
|
62
|
+
if (activeWorkspaceId !== workspaceId && watches.has(key))
|
|
63
63
|
this.remove(activeWorkspaceId, key);
|
|
64
|
+
const previous = this.state.watches.get(workspaceId)?.get(key);
|
|
64
65
|
const watch = {
|
|
65
66
|
workspaceId,
|
|
66
67
|
watchId,
|
|
@@ -76,6 +77,12 @@ export class WorkspaceWatchService {
|
|
|
76
77
|
const watches = this.state.watches.get(workspaceId) ?? new Map();
|
|
77
78
|
watches.set(key, watch);
|
|
78
79
|
this.state.watches.set(workspaceId, watches);
|
|
80
|
+
if (previous !== undefined) {
|
|
81
|
+
clearTimeout(previous.timer);
|
|
82
|
+
this.onWatchRemoved(previous);
|
|
83
|
+
if (previous.topics.sessions && !this.topicActive(workspaceId, 'sessions'))
|
|
84
|
+
this.onSessionWatchUnused(workspaceId);
|
|
85
|
+
}
|
|
79
86
|
if (topics.sessions)
|
|
80
87
|
this.onSessionWatchRequired(workspaceId);
|
|
81
88
|
return watch;
|
|
@@ -7,17 +7,28 @@ export class WorkspaceWatchSnapshotService {
|
|
|
7
7
|
dirty = new Set();
|
|
8
8
|
reads = new Map();
|
|
9
9
|
signatures = new Map();
|
|
10
|
+
// Handoff only between consecutive group selections on the same page/Workspace.
|
|
11
|
+
// Weak keys release the two resource payloads with their lease, not a second TTL cache.
|
|
12
|
+
resourceSnapshots = new WeakMap();
|
|
10
13
|
filesRefreshTimer;
|
|
11
14
|
constructor(options) {
|
|
12
15
|
this.options = options;
|
|
13
16
|
}
|
|
14
|
-
async initial(workspace, watch) {
|
|
17
|
+
async initial(workspace, watch, previous) {
|
|
15
18
|
if (watch.topics.files !== undefined)
|
|
16
19
|
this.ensureFilesRefreshTimer();
|
|
17
20
|
await Promise.all(this.subscribedTopics(watch).map(async (topic) => {
|
|
18
21
|
const revision = this.readRevision(workspace.id, topic);
|
|
19
22
|
try {
|
|
20
|
-
const
|
|
23
|
+
const retained = topic === 'sessions' || previous === undefined
|
|
24
|
+
? undefined
|
|
25
|
+
: this.resourceSnapshots.get(previous)?.[topic];
|
|
26
|
+
const payload = retained !== undefined &&
|
|
27
|
+
retained.revision === revision &&
|
|
28
|
+
!this.readActive(workspace.id, topic, watch) &&
|
|
29
|
+
!this.refreshes.has(`${workspace.id}:${topic}`)
|
|
30
|
+
? retained.payload
|
|
31
|
+
: await this.read(workspace, topic, watch);
|
|
21
32
|
if (!this.matches(workspace.id, watch))
|
|
22
33
|
return;
|
|
23
34
|
if (revision !== this.readRevision(workspace.id, topic)) {
|
|
@@ -218,7 +229,9 @@ export class WorkspaceWatchSnapshotService {
|
|
|
218
229
|
? `${workspaceId}:files:${JSON.stringify(watch.topics.files)}`
|
|
219
230
|
: `${workspaceId}:${topic}:${watch.topics.sessionGroup ?? 'ALL'}`;
|
|
220
231
|
}
|
|
221
|
-
readActive(workspaceId, topic) {
|
|
232
|
+
readActive(workspaceId, topic, watch) {
|
|
233
|
+
if (topic === 'files' && watch !== undefined)
|
|
234
|
+
return this.reads.has(this.viewKey(workspaceId, topic, watch));
|
|
222
235
|
const prefix = `${workspaceId}:${topic}`;
|
|
223
236
|
return [...this.reads.keys()].some((key) => key === prefix || key.startsWith(`${prefix}:`));
|
|
224
237
|
}
|
|
@@ -237,6 +250,11 @@ export class WorkspaceWatchSnapshotService {
|
|
|
237
250
|
deliver(workspace, watch, topic, payload) {
|
|
238
251
|
if (!this.matches(workspace.id, watch))
|
|
239
252
|
return;
|
|
253
|
+
if (topic !== 'sessions') {
|
|
254
|
+
const snapshots = this.resourceSnapshots.get(watch) ?? {};
|
|
255
|
+
snapshots[topic] = { payload, revision: this.readRevision(workspace.id, topic) };
|
|
256
|
+
this.resourceSnapshots.set(watch, snapshots);
|
|
257
|
+
}
|
|
240
258
|
this.options.emitWatchEvent({
|
|
241
259
|
type: 'watch.workspace.snapshot',
|
|
242
260
|
workbenchConnectionId: watch.workbenchConnectionId,
|
|
@@ -254,6 +272,11 @@ export class WorkspaceWatchSnapshotService {
|
|
|
254
272
|
fail(workspaceId, watch, topic) {
|
|
255
273
|
if (!this.matches(workspaceId, watch))
|
|
256
274
|
return;
|
|
275
|
+
if (topic !== 'sessions') {
|
|
276
|
+
const snapshots = this.resourceSnapshots.get(watch);
|
|
277
|
+
if (snapshots !== undefined)
|
|
278
|
+
delete snapshots[topic];
|
|
279
|
+
}
|
|
257
280
|
this.options.emitWatchEvent({
|
|
258
281
|
type: 'watch.failed',
|
|
259
282
|
workbenchConnectionId: watch.workbenchConnectionId,
|
|
@@ -131,11 +131,15 @@ export function validUserAuthorization(value, envelope, operation, nodeId, now =
|
|
|
131
131
|
if (typeof value !== 'object' || value === null || nodeId === undefined)
|
|
132
132
|
return false;
|
|
133
133
|
const authorization = value;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
authorization.
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
const modelResourceCapability = operation === 'model.resource.read' &&
|
|
135
|
+
authorization.userId === 0 &&
|
|
136
|
+
authorization.sessionHash === 'model-resource';
|
|
137
|
+
return ((modelResourceCapability ||
|
|
138
|
+
(typeof authorization.userId === 'number' &&
|
|
139
|
+
Number.isSafeInteger(authorization.userId) &&
|
|
140
|
+
authorization.userId > 0 &&
|
|
141
|
+
typeof authorization.sessionHash === 'string' &&
|
|
142
|
+
/^[A-Za-z0-9_-]{43}$/.test(authorization.sessionHash))) &&
|
|
139
143
|
authorization.nodeId === nodeId &&
|
|
140
144
|
authorization.operation === operation &&
|
|
141
145
|
authorization.requestId === envelope.id &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.22",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"playwright": "1.62.1",
|
|
38
38
|
"ws": "^8.21.3",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@myagentroam/agent": "0.11.
|
|
40
|
+
"@myagentroam/agent": "0.11.22",
|
|
41
41
|
"@myagentroam/marctl": "0.11.0",
|
|
42
|
-
"@myagentroam/protocol": "0.11.
|
|
42
|
+
"@myagentroam/protocol": "0.11.22",
|
|
43
43
|
"@myagentroam/code-navigation": "0.11.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|