@valon-technologies/gestalt 0.0.1-alpha.13 → 0.0.1-alpha.14

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.
@@ -0,0 +1,244 @@
1
+ import { connect } from "node:net";
2
+ import { Writable } from "node:stream";
3
+
4
+ import type { MessageInitShape } from "@bufbuild/protobuf";
5
+ import {
6
+ createClient,
7
+ type Client,
8
+ type Interceptor,
9
+ } from "@connectrpc/connect";
10
+ import { createGrpcTransport } from "@connectrpc/connect-node";
11
+
12
+ import {
13
+ AppendPluginRuntimeLogsRequestSchema,
14
+ type AppendPluginRuntimeLogsResponse,
15
+ PluginRuntimeLogHost as PluginRuntimeLogHostService,
16
+ PluginRuntimeLogStream,
17
+ } from "../gen/v1/pluginruntime_pb.ts";
18
+
19
+ export const ENV_RUNTIME_LOG_HOST_SOCKET = "GESTALT_RUNTIME_LOG_SOCKET";
20
+ export const ENV_RUNTIME_LOG_HOST_SOCKET_TOKEN = `${ENV_RUNTIME_LOG_HOST_SOCKET}_TOKEN`;
21
+ export const ENV_RUNTIME_SESSION_ID = "GESTALT_RUNTIME_SESSION_ID";
22
+
23
+ const RUNTIME_LOG_RELAY_TOKEN_HEADER = "x-gestalt-host-service-relay-token";
24
+
25
+ export type RuntimeLogStreamName = "stdout" | "stderr" | "runtime";
26
+ export type RuntimeLogStreamInput =
27
+ | RuntimeLogStreamName
28
+ | PluginRuntimeLogStream;
29
+ export type RuntimeLogAppendLogsInput = MessageInitShape<
30
+ typeof AppendPluginRuntimeLogsRequestSchema
31
+ >;
32
+ export type RuntimeLogAppendResponseMessage = AppendPluginRuntimeLogsResponse;
33
+
34
+ export interface RuntimeLogAppendInput {
35
+ sessionId?: string;
36
+ message: string | Uint8Array;
37
+ stream?: RuntimeLogStreamInput;
38
+ observedAt?: Date;
39
+ sourceSeq?: number | bigint;
40
+ }
41
+
42
+ export interface RuntimeLogWriterOptions {
43
+ sessionId?: string;
44
+ stream?: RuntimeLogStreamInput;
45
+ sourceSeqStart?: number | bigint;
46
+ }
47
+
48
+ export class RuntimeLogHost {
49
+ private readonly client: Client<typeof PluginRuntimeLogHostService>;
50
+ private sourceSeq = 0n;
51
+
52
+ constructor() {
53
+ const target = process.env[ENV_RUNTIME_LOG_HOST_SOCKET];
54
+ if (!target) {
55
+ throw new Error(
56
+ `runtime log host: ${ENV_RUNTIME_LOG_HOST_SOCKET} is not set`,
57
+ );
58
+ }
59
+ const relayToken =
60
+ process.env[ENV_RUNTIME_LOG_HOST_SOCKET_TOKEN]?.trim() ?? "";
61
+ const transportOptions = runtimeLogTransportOptions(target);
62
+ const transport = createGrpcTransport({
63
+ ...transportOptions,
64
+ ...(transportOptions.nodeOptions
65
+ ? {
66
+ nodeOptions: {
67
+ createConnection: () =>
68
+ connect(transportOptions.nodeOptions!.path),
69
+ },
70
+ }
71
+ : {}),
72
+ interceptors: relayToken
73
+ ? [runtimeLogRelayTokenInterceptor(relayToken)]
74
+ : [],
75
+ });
76
+ this.client = createClient(PluginRuntimeLogHostService, transport);
77
+ }
78
+
79
+ async appendLogs(
80
+ request: RuntimeLogAppendLogsInput,
81
+ ): Promise<RuntimeLogAppendResponseMessage> {
82
+ return await this.client.appendLogs(request);
83
+ }
84
+
85
+ async append(
86
+ input: RuntimeLogAppendInput,
87
+ ): Promise<RuntimeLogAppendResponseMessage> {
88
+ const sourceSeq =
89
+ input.sourceSeq === undefined
90
+ ? (this.sourceSeq += 1n)
91
+ : BigInt(input.sourceSeq);
92
+ if (sourceSeq > this.sourceSeq) {
93
+ this.sourceSeq = sourceSeq;
94
+ }
95
+ return await this.appendLogs({
96
+ sessionId: runtimeSessionId(input.sessionId),
97
+ logs: [
98
+ {
99
+ stream: runtimeLogStream(input.stream ?? "runtime"),
100
+ message: runtimeLogMessage(input.message),
101
+ observedAt: toProtoTimestamp(input.observedAt ?? new Date()),
102
+ sourceSeq,
103
+ },
104
+ ],
105
+ });
106
+ }
107
+
108
+ writer(options?: RuntimeLogWriterOptions): Writable;
109
+ writer(sessionId: string, options?: RuntimeLogWriterOptions): Writable;
110
+ writer(
111
+ sessionIdOrOptions: string | RuntimeLogWriterOptions = {},
112
+ options: RuntimeLogWriterOptions = {},
113
+ ): Writable {
114
+ const writerOptions =
115
+ typeof sessionIdOrOptions === "string"
116
+ ? options
117
+ : sessionIdOrOptions;
118
+ const sessionId = runtimeSessionId(
119
+ typeof sessionIdOrOptions === "string"
120
+ ? sessionIdOrOptions
121
+ : writerOptions.sessionId,
122
+ );
123
+ const stream = writerOptions.stream ?? "stdout";
124
+ let sourceSeq = BigInt(writerOptions.sourceSeqStart ?? 0);
125
+
126
+ return new Writable({
127
+ write: (chunk: Buffer | string, encoding, callback) => {
128
+ const actualEncoding = (
129
+ String(encoding) === "buffer" ? "utf8" : encoding
130
+ ) as BufferEncoding;
131
+ const message =
132
+ typeof chunk === "string"
133
+ ? chunk
134
+ : Buffer.from(chunk).toString(actualEncoding);
135
+ sourceSeq += 1n;
136
+ this.append({
137
+ sessionId,
138
+ stream,
139
+ message,
140
+ sourceSeq,
141
+ }).then(
142
+ () => callback(),
143
+ (error: unknown) => callback(toError(error)),
144
+ );
145
+ },
146
+ });
147
+ }
148
+ }
149
+
150
+ function runtimeSessionId(sessionId?: string): string {
151
+ const value = (sessionId ?? process.env[ENV_RUNTIME_SESSION_ID] ?? "").trim();
152
+ if (!value) {
153
+ throw new Error(`runtime session: ${ENV_RUNTIME_SESSION_ID} is not set`);
154
+ }
155
+ return value;
156
+ }
157
+
158
+ function runtimeLogTransportOptions(rawTarget: string): {
159
+ baseUrl: string;
160
+ nodeOptions?: { path: string };
161
+ } {
162
+ const target = rawTarget.trim();
163
+ if (!target) {
164
+ throw new Error("runtime log host: transport target is required");
165
+ }
166
+ if (target.startsWith("tcp://")) {
167
+ const address = target.slice("tcp://".length).trim();
168
+ if (!address) {
169
+ throw new Error(
170
+ `runtime log host: tcp target ${JSON.stringify(rawTarget)} is missing host:port`,
171
+ );
172
+ }
173
+ return { baseUrl: `http://${address}` };
174
+ }
175
+ if (target.startsWith("tls://")) {
176
+ const address = target.slice("tls://".length).trim();
177
+ if (!address) {
178
+ throw new Error(
179
+ `runtime log host: tls target ${JSON.stringify(rawTarget)} is missing host:port`,
180
+ );
181
+ }
182
+ return { baseUrl: `https://${address}` };
183
+ }
184
+ if (target.startsWith("unix://")) {
185
+ const socketPath = target.slice("unix://".length).trim();
186
+ if (!socketPath) {
187
+ throw new Error(
188
+ `runtime log host: unix target ${JSON.stringify(rawTarget)} is missing a socket path`,
189
+ );
190
+ }
191
+ return { baseUrl: "http://localhost", nodeOptions: { path: socketPath } };
192
+ }
193
+ if (target.includes("://")) {
194
+ const parsed = new URL(target);
195
+ throw new Error(
196
+ `runtime log host: unsupported target scheme ${JSON.stringify(parsed.protocol.replace(/:$/, ""))}`,
197
+ );
198
+ }
199
+ return { baseUrl: "http://localhost", nodeOptions: { path: target } };
200
+ }
201
+
202
+ function runtimeLogRelayTokenInterceptor(token: string): Interceptor {
203
+ return (next) => async (req) => {
204
+ req.header.set(RUNTIME_LOG_RELAY_TOKEN_HEADER, token);
205
+ return await next(req);
206
+ };
207
+ }
208
+
209
+ function runtimeLogStream(stream: RuntimeLogStreamInput): PluginRuntimeLogStream {
210
+ if (typeof stream === "number") {
211
+ return stream;
212
+ }
213
+ switch (stream.trim().toLowerCase()) {
214
+ case "stdout":
215
+ return PluginRuntimeLogStream.STDOUT;
216
+ case "stderr":
217
+ return PluginRuntimeLogStream.STDERR;
218
+ case "runtime":
219
+ return PluginRuntimeLogStream.RUNTIME;
220
+ default:
221
+ throw new Error(`unsupported runtime log stream ${JSON.stringify(stream)}`);
222
+ }
223
+ }
224
+
225
+ function runtimeLogMessage(message: string | Uint8Array): string {
226
+ if (typeof message === "string") {
227
+ return message;
228
+ }
229
+ return Buffer.from(message).toString("utf8");
230
+ }
231
+
232
+ function toProtoTimestamp(value: Date): { seconds: bigint; nanos: number } {
233
+ const millis = value.getTime();
234
+ const seconds = Math.floor(millis / 1000);
235
+ const nanos = Math.trunc((millis - (seconds * 1000)) * 1_000_000);
236
+ return {
237
+ seconds: BigInt(seconds),
238
+ nanos,
239
+ };
240
+ }
241
+
242
+ function toError(error: unknown): Error {
243
+ return error instanceof Error ? error : new Error(String(error));
244
+ }
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, rmSync, writeFileSync } from "node:fs";
1
+ import { rmSync, writeFileSync } from "node:fs";
2
2
  import { createServer } from "node:http2";
3
3
  import { dirname, resolve } from "node:path";
4
4
 
@@ -48,7 +48,7 @@ import {
48
48
  OperationResultSchema,
49
49
  ProviderMetadataSchema,
50
50
  type HTTPSubjectRequest as ProtoHTTPSubjectRequest,
51
- type IntegrationToken as ProtoIntegrationToken,
51
+ type PostConnectCredential as ProtoPostConnectCredential,
52
52
  type RequestContext as ProtoRequestContext,
53
53
  type ResolveHTTPSubjectRequest as ProtoResolveHTTPSubjectRequest,
54
54
  IntegrationProvider as IntegrationProviderService,
@@ -57,12 +57,16 @@ import {
57
57
  type GetSessionCatalogRequest,
58
58
  type StartProviderRequest,
59
59
  } from "../gen/v1/plugin_pb.ts";
60
+ import {
61
+ PluginRuntimeProvider as PluginRuntimeProviderService,
62
+ } from "../gen/v1/pluginruntime_pb.ts";
60
63
  import {
61
64
  ConfigureProviderResponseSchema,
62
65
  HealthCheckResponseSchema,
63
66
  ProviderIdentitySchema,
64
67
  ProviderKind as ProtoProviderKind,
65
68
  ProviderLifecycle,
69
+ StartRuntimeProviderResponseSchema,
66
70
  type ConfigureProviderRequest,
67
71
  } from "../gen/v1/runtime_pb.ts";
68
72
  import { S3 as S3Service } from "../gen/v1/s3_pb.ts";
@@ -93,6 +97,11 @@ import {
93
97
  connectionParamToProto,
94
98
  isPluginProvider,
95
99
  } from "./plugin.ts";
100
+ import {
101
+ PluginRuntimeProvider,
102
+ createPluginRuntimeProviderService,
103
+ isPluginRuntimeProvider,
104
+ } from "./pluginruntime.ts";
96
105
  import {
97
106
  providerKindLabel,
98
107
  resolveDefaultProviderExport,
@@ -125,11 +134,6 @@ export const ENV_PROVIDER_PARENT_PID = "GESTALT_PLUGIN_PARENT_PID";
125
134
  * Environment variable used to request static catalog generation.
126
135
  */
127
136
  export const ENV_WRITE_CATALOG = "GESTALT_PLUGIN_WRITE_CATALOG";
128
- /**
129
- * Environment variable used to request generated manifest metadata export.
130
- */
131
- export const ENV_WRITE_MANIFEST_METADATA =
132
- "GESTALT_PLUGIN_WRITE_MANIFEST_METADATA";
133
137
  /**
134
138
  * Protocol version currently implemented by the TypeScript runtime.
135
139
  */
@@ -139,6 +143,7 @@ export const CURRENT_PROTOCOL_VERSION = 3;
139
143
  */
140
144
  export const USAGE = "usage: bun run runtime.ts ROOT PROVIDER_TARGET";
141
145
  export { createAgentProviderService } from "./agent.ts";
146
+ export { createPluginRuntimeProviderService } from "./pluginruntime.ts";
142
147
  export { createWorkflowProviderService } from "./workflow.ts";
143
148
 
144
149
  /**
@@ -158,6 +163,7 @@ export type LoadedProvider =
158
163
  | CacheProvider
159
164
  | SecretsProvider
160
165
  | S3Provider
166
+ | PluginRuntimeProvider
161
167
  | AgentProvider
162
168
  | WorkflowProvider;
163
169
 
@@ -218,6 +224,17 @@ const PROVIDER_RUNTIME_ENTRIES: Partial<
218
224
  router.service(S3Service, createS3Service(provider as S3Provider));
219
225
  },
220
226
  },
227
+ runtime: {
228
+ isProvider:
229
+ isPluginRuntimeProvider as (value: unknown) => value is LoadedProvider,
230
+ protoKind: ProtoProviderKind.RUNTIME,
231
+ registerService(router, provider) {
232
+ router.service(
233
+ PluginRuntimeProviderService,
234
+ createPluginRuntimeProviderService(provider as PluginRuntimeProvider),
235
+ );
236
+ },
237
+ },
221
238
  agent: {
222
239
  isProvider: isAgentProvider as (value: unknown) => value is LoadedProvider,
223
240
  protoKind: ProtoProviderKind.AGENT,
@@ -324,19 +341,11 @@ export async function runLoadedProvider(
324
341
  }
325
342
 
326
343
  const catalogPath = process.env[ENV_WRITE_CATALOG];
327
- const manifestMetadataPath = process.env[ENV_WRITE_MANIFEST_METADATA];
328
- if (catalogPath || manifestMetadataPath) {
344
+ if (catalogPath) {
329
345
  if (!isPluginProvider(provider)) {
330
- throw new Error(
331
- "static catalog and manifest metadata generation are only supported for plugin providers",
332
- );
333
- }
334
- if (catalogPath) {
335
- writeFileSync(catalogPath, catalogToYaml(provider.staticCatalog()), "utf8");
336
- }
337
- if (manifestMetadataPath && provider.supportsManifestMetadata()) {
338
- provider.writeManifestMetadata(manifestMetadataPath);
346
+ throw new Error("static catalog generation is only supported for plugin providers");
339
347
  }
348
+ writeFileSync(catalogPath, catalogToYaml(provider.staticCatalog()), "utf8");
340
349
  return;
341
350
  }
342
351
 
@@ -364,9 +373,7 @@ export async function serve(provider: LoadedProvider): Promise<void> {
364
373
  if (!socketPath) {
365
374
  throw new Error(`${ENV_PROVIDER_SOCKET} is required`);
366
375
  }
367
- if (existsSync(socketPath)) {
368
- rmSync(socketPath);
369
- }
376
+ rmSync(socketPath, { force: true });
370
377
 
371
378
  const handler = connectNodeAdapter({
372
379
  grpc: true,
@@ -393,9 +400,7 @@ export async function serve(provider: LoadedProvider): Promise<void> {
393
400
  server.close(() => resolveClose());
394
401
  });
395
402
  } finally {
396
- if (existsSync(socketPath)) {
397
- rmSync(socketPath);
398
- }
403
+ rmSync(socketPath, { force: true });
399
404
  }
400
405
  }
401
406
  })();
@@ -482,6 +487,16 @@ export function createRuntimeService(
482
487
  });
483
488
  }
484
489
  },
490
+ async startProvider() {
491
+ try {
492
+ await provider.startProvider();
493
+ } catch (error) {
494
+ throw providerRuntimeError("start provider", error);
495
+ }
496
+ return create(StartRuntimeProviderResponseSchema, {
497
+ protocolVersion: CURRENT_PROTOCOL_VERSION,
498
+ });
499
+ },
485
500
  };
486
501
  }
487
502
 
@@ -548,6 +563,7 @@ export function createProviderService(
548
563
  request.connectionParams,
549
564
  request.context,
550
565
  request.invocationToken,
566
+ request.idempotencyKey,
551
567
  ),
552
568
  ),
553
569
  );
@@ -801,6 +817,7 @@ function providerRequest(
801
817
  connectionParams: Record<string, string>,
802
818
  requestContext?: ProtoRequestContext,
803
819
  invocationToken = "",
820
+ idempotencyKey = "",
804
821
  ): Request {
805
822
  const subject = requestContext?.subject;
806
823
  const credential = requestContext?.credential;
@@ -830,6 +847,7 @@ function providerRequest(
830
847
  ...(requestContext?.workflow ?? {}),
831
848
  },
832
849
  invocationToken,
850
+ idempotencyKey: idempotencyKey.trim(),
833
851
  };
834
852
  }
835
853
 
@@ -866,12 +884,12 @@ function providerHTTPSubjectResolutionContext(
866
884
  }
867
885
 
868
886
  function providerConnectedToken(
869
- token?: ProtoIntegrationToken,
887
+ token?: ProtoPostConnectCredential,
870
888
  ): ConnectedToken {
871
889
  const metadataJson = token?.metadataJson ?? "";
872
890
  return {
873
891
  id: token?.id ?? "",
874
- subjectId: token?.userId ?? "",
892
+ subjectId: token?.subjectId ?? "",
875
893
  integration: token?.integration ?? "",
876
894
  connection: token?.connection ?? "",
877
895
  instance: token?.instance ?? "",
package/src/s3.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  PresignObjectResponseSchema,
21
21
  ReadObjectChunkSchema,
22
22
  S3 as S3Service,
23
+ S3ObjectAccess as S3ObjectAccessService,
23
24
  type ByteRange as ProtoByteRange,
24
25
  type ReadObjectRequest as ProtoReadObjectRequest,
25
26
  type S3ObjectMeta as ProtoS3ObjectMeta,
@@ -198,6 +199,16 @@ export interface PresignResult {
198
199
  headers: Record<string, string>;
199
200
  }
200
201
 
202
+ /**
203
+ * Options used when creating a host-mediated object-access URL.
204
+ */
205
+ export type ObjectAccessURLOptions = PresignOptions;
206
+
207
+ /**
208
+ * Result returned by {@link S3.createObjectAccessURL} or {@link S3Object.createAccessURL}.
209
+ */
210
+ export type ObjectAccessURL = PresignResult;
211
+
201
212
  /**
202
213
  * Accepted write body sources for the S3 client.
203
214
  */
@@ -509,7 +520,9 @@ export function createS3Service(
509
520
  * ```
510
521
  */
511
522
  export class S3 {
523
+ private readonly transport: ReturnType<typeof createGrpcTransport>;
512
524
  private readonly client: Client<typeof S3Service>;
525
+ private objectAccessClient?: Client<typeof S3ObjectAccessService>;
513
526
 
514
527
  constructor(name?: string) {
515
528
  const envName = s3SocketEnv(name);
@@ -538,6 +551,7 @@ export class S3 {
538
551
  : {}),
539
552
  interceptors,
540
553
  });
554
+ this.transport = transport;
541
555
  this.client = createClient(S3Service, transport);
542
556
  }
543
557
 
@@ -654,6 +668,62 @@ export class S3 {
654
668
  }
655
669
  return result;
656
670
  }
671
+
672
+ async createObjectAccessURL(
673
+ ref: ObjectRef,
674
+ options?: ObjectAccessURLOptions,
675
+ ): Promise<ObjectAccessURL> {
676
+ const requestedMethod = options?.method ?? PresignMethod.Get;
677
+ const response = await s3Rpc(() =>
678
+ this.s3ObjectAccessClient().createObjectAccessURL({
679
+ ref: toProtoObjectRef(ref),
680
+ method: toProtoPresignMethod(requestedMethod),
681
+ expiresSeconds: normalizeProtoInt(options?.expiresSeconds),
682
+ contentType: options?.contentType ?? "",
683
+ contentDisposition: options?.contentDisposition ?? "",
684
+ headers: cloneStringMap(options?.headers),
685
+ }),
686
+ );
687
+ const result: ObjectAccessURL = {
688
+ url: response.url,
689
+ method: response.method === ProtoPresignMethod.UNSPECIFIED
690
+ ? requestedMethod
691
+ : fromProtoPresignMethod(response.method),
692
+ headers: cloneStringMap(response.headers),
693
+ };
694
+ if (response.expiresAt) {
695
+ result.expiresAt = fromProtoTimestamp(response.expiresAt);
696
+ }
697
+ return result;
698
+ }
699
+
700
+ private s3ObjectAccessClient(): Client<typeof S3ObjectAccessService> {
701
+ if (!this.objectAccessClient) {
702
+ this.objectAccessClient = createClient(S3ObjectAccessService, this.transport);
703
+ }
704
+ return this.objectAccessClient;
705
+ }
706
+
707
+ async createObjectAccessUrl(
708
+ ref: ObjectRef,
709
+ options?: ObjectAccessURLOptions,
710
+ ): Promise<ObjectAccessURL> {
711
+ return await this.createObjectAccessURL(ref, options);
712
+ }
713
+
714
+ async createAccessURL(
715
+ ref: ObjectRef,
716
+ options?: ObjectAccessURLOptions,
717
+ ): Promise<ObjectAccessURL> {
718
+ return await this.createObjectAccessURL(ref, options);
719
+ }
720
+
721
+ async createAccessUrl(
722
+ ref: ObjectRef,
723
+ options?: ObjectAccessURLOptions,
724
+ ): Promise<ObjectAccessURL> {
725
+ return await this.createObjectAccessURL(ref, options);
726
+ }
657
727
  }
658
728
 
659
729
  /**
@@ -768,6 +838,17 @@ export class S3Object {
768
838
  async presign(options?: PresignOptions): Promise<PresignResult> {
769
839
  return await this.client.presignObject(this.ref, options);
770
840
  }
841
+
842
+ /**
843
+ * Creates a host-mediated object-access URL for the referenced object.
844
+ */
845
+ async createAccessURL(options?: ObjectAccessURLOptions): Promise<ObjectAccessURL> {
846
+ return await this.client.createObjectAccessURL(this.ref, options);
847
+ }
848
+
849
+ async createAccessUrl(options?: ObjectAccessURLOptions): Promise<ObjectAccessURL> {
850
+ return await this.createAccessURL(options);
851
+ }
771
852
  }
772
853
 
773
854
  function s3TransportOptions(rawTarget: string): {