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

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,6 @@
1
- import { existsSync, rmSync, writeFileSync } from "node:fs";
1
+ #!/usr/bin/env bun
2
+
3
+ import { rmSync, writeFileSync } from "node:fs";
2
4
  import { createServer } from "node:http2";
3
5
  import { dirname, resolve } from "node:path";
4
6
 
@@ -48,7 +50,7 @@ import {
48
50
  OperationResultSchema,
49
51
  ProviderMetadataSchema,
50
52
  type HTTPSubjectRequest as ProtoHTTPSubjectRequest,
51
- type IntegrationToken as ProtoIntegrationToken,
53
+ type PostConnectCredential as ProtoPostConnectCredential,
52
54
  type RequestContext as ProtoRequestContext,
53
55
  type ResolveHTTPSubjectRequest as ProtoResolveHTTPSubjectRequest,
54
56
  IntegrationProvider as IntegrationProviderService,
@@ -57,12 +59,16 @@ import {
57
59
  type GetSessionCatalogRequest,
58
60
  type StartProviderRequest,
59
61
  } from "../gen/v1/plugin_pb.ts";
62
+ import {
63
+ PluginRuntimeProvider as PluginRuntimeProviderService,
64
+ } from "../gen/v1/pluginruntime_pb.ts";
60
65
  import {
61
66
  ConfigureProviderResponseSchema,
62
67
  HealthCheckResponseSchema,
63
68
  ProviderIdentitySchema,
64
69
  ProviderKind as ProtoProviderKind,
65
70
  ProviderLifecycle,
71
+ StartRuntimeProviderResponseSchema,
66
72
  type ConfigureProviderRequest,
67
73
  } from "../gen/v1/runtime_pb.ts";
68
74
  import { S3 as S3Service } from "../gen/v1/s3_pb.ts";
@@ -93,6 +99,11 @@ import {
93
99
  connectionParamToProto,
94
100
  isPluginProvider,
95
101
  } from "./plugin.ts";
102
+ import {
103
+ PluginRuntimeProvider,
104
+ createPluginRuntimeProviderService,
105
+ isPluginRuntimeProvider,
106
+ } from "./pluginruntime.ts";
96
107
  import {
97
108
  providerKindLabel,
98
109
  resolveDefaultProviderExport,
@@ -125,11 +136,6 @@ export const ENV_PROVIDER_PARENT_PID = "GESTALT_PLUGIN_PARENT_PID";
125
136
  * Environment variable used to request static catalog generation.
126
137
  */
127
138
  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
139
  /**
134
140
  * Protocol version currently implemented by the TypeScript runtime.
135
141
  */
@@ -139,6 +145,7 @@ export const CURRENT_PROTOCOL_VERSION = 3;
139
145
  */
140
146
  export const USAGE = "usage: bun run runtime.ts ROOT PROVIDER_TARGET";
141
147
  export { createAgentProviderService } from "./agent.ts";
148
+ export { createPluginRuntimeProviderService } from "./pluginruntime.ts";
142
149
  export { createWorkflowProviderService } from "./workflow.ts";
143
150
 
144
151
  /**
@@ -158,6 +165,7 @@ export type LoadedProvider =
158
165
  | CacheProvider
159
166
  | SecretsProvider
160
167
  | S3Provider
168
+ | PluginRuntimeProvider
161
169
  | AgentProvider
162
170
  | WorkflowProvider;
163
171
 
@@ -218,6 +226,17 @@ const PROVIDER_RUNTIME_ENTRIES: Partial<
218
226
  router.service(S3Service, createS3Service(provider as S3Provider));
219
227
  },
220
228
  },
229
+ runtime: {
230
+ isProvider:
231
+ isPluginRuntimeProvider as (value: unknown) => value is LoadedProvider,
232
+ protoKind: ProtoProviderKind.RUNTIME,
233
+ registerService(router, provider) {
234
+ router.service(
235
+ PluginRuntimeProviderService,
236
+ createPluginRuntimeProviderService(provider as PluginRuntimeProvider),
237
+ );
238
+ },
239
+ },
221
240
  agent: {
222
241
  isProvider: isAgentProvider as (value: unknown) => value is LoadedProvider,
223
242
  protoKind: ProtoProviderKind.AGENT,
@@ -324,19 +343,11 @@ export async function runLoadedProvider(
324
343
  }
325
344
 
326
345
  const catalogPath = process.env[ENV_WRITE_CATALOG];
327
- const manifestMetadataPath = process.env[ENV_WRITE_MANIFEST_METADATA];
328
- if (catalogPath || manifestMetadataPath) {
346
+ if (catalogPath) {
329
347
  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);
348
+ throw new Error("static catalog generation is only supported for plugin providers");
339
349
  }
350
+ writeFileSync(catalogPath, catalogToYaml(provider.staticCatalog()), "utf8");
340
351
  return;
341
352
  }
342
353
 
@@ -364,9 +375,7 @@ export async function serve(provider: LoadedProvider): Promise<void> {
364
375
  if (!socketPath) {
365
376
  throw new Error(`${ENV_PROVIDER_SOCKET} is required`);
366
377
  }
367
- if (existsSync(socketPath)) {
368
- rmSync(socketPath);
369
- }
378
+ rmSync(socketPath, { force: true });
370
379
 
371
380
  const handler = connectNodeAdapter({
372
381
  grpc: true,
@@ -393,9 +402,7 @@ export async function serve(provider: LoadedProvider): Promise<void> {
393
402
  server.close(() => resolveClose());
394
403
  });
395
404
  } finally {
396
- if (existsSync(socketPath)) {
397
- rmSync(socketPath);
398
- }
405
+ rmSync(socketPath, { force: true });
399
406
  }
400
407
  }
401
408
  })();
@@ -482,6 +489,16 @@ export function createRuntimeService(
482
489
  });
483
490
  }
484
491
  },
492
+ async startProvider() {
493
+ try {
494
+ await provider.startProvider();
495
+ } catch (error) {
496
+ throw providerRuntimeError("start provider", error);
497
+ }
498
+ return create(StartRuntimeProviderResponseSchema, {
499
+ protocolVersion: CURRENT_PROTOCOL_VERSION,
500
+ });
501
+ },
485
502
  };
486
503
  }
487
504
 
@@ -548,6 +565,7 @@ export function createProviderService(
548
565
  request.connectionParams,
549
566
  request.context,
550
567
  request.invocationToken,
568
+ request.idempotencyKey,
551
569
  ),
552
570
  ),
553
571
  );
@@ -801,6 +819,7 @@ function providerRequest(
801
819
  connectionParams: Record<string, string>,
802
820
  requestContext?: ProtoRequestContext,
803
821
  invocationToken = "",
822
+ idempotencyKey = "",
804
823
  ): Request {
805
824
  const subject = requestContext?.subject;
806
825
  const credential = requestContext?.credential;
@@ -830,6 +849,7 @@ function providerRequest(
830
849
  ...(requestContext?.workflow ?? {}),
831
850
  },
832
851
  invocationToken,
852
+ idempotencyKey: idempotencyKey.trim(),
833
853
  };
834
854
  }
835
855
 
@@ -866,12 +886,12 @@ function providerHTTPSubjectResolutionContext(
866
886
  }
867
887
 
868
888
  function providerConnectedToken(
869
- token?: ProtoIntegrationToken,
889
+ token?: ProtoPostConnectCredential,
870
890
  ): ConnectedToken {
871
891
  const metadataJson = token?.metadataJson ?? "";
872
892
  return {
873
893
  id: token?.id ?? "",
874
- subjectId: token?.userId ?? "",
894
+ subjectId: token?.subjectId ?? "",
875
895
  integration: token?.integration ?? "",
876
896
  connection: token?.connection ?? "",
877
897
  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): {