@tomflow/proflow-agent-gateway 0.1.17 → 0.1.19

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @tomflow/proflow-agent-gateway
2
2
 
3
+ ## 0.1.19
4
+
5
+ ### Patch Changes
6
+
7
+ - Preserve allowlisted downstream tool error codes across Gateway HTTP translation while keeping unknown failures opaque, and stop generic Gateway logs from fabricating side-effect state.
8
+
9
+ - Fail closed when the public Gateway role credential store becomes group/world-readable on POSIX systems.
10
+
11
+ ## 0.1.18
12
+
13
+ ### Patch Changes
14
+
15
+ - Aggregate Browser, Permission, Host-application, Provisioning, Direct Tool, and Gateway observability at stable runtime boundaries, preserving owner truth while enabling one operation-chain diagnosis path without logging sensitive payloads.
16
+
3
17
  ## 0.1.17
4
18
 
5
19
  ### Patch Changes
@@ -6,7 +6,7 @@ export declare const behaviorAdapter: {
6
6
  ok: true;
7
7
  status: "SUCCEEDED";
8
8
  moduleRef: "agent-gateway";
9
- moduleVersion: "0.1.17";
9
+ moduleVersion: "0.1.19";
10
10
  data: {
11
11
  localBaseUrl: string;
12
12
  publicBaseUrl?: string;
@@ -20,7 +20,7 @@ export declare const behaviorAdapter: {
20
20
  readonly ok: true;
21
21
  readonly status: "SUCCEEDED";
22
22
  readonly moduleRef: "agent-gateway";
23
- readonly moduleVersion: "0.1.17";
23
+ readonly moduleVersion: "0.1.19";
24
24
  };
25
25
  observedEffects: string[];
26
26
  }>;
@@ -30,7 +30,7 @@ export declare const behaviorAdapter: {
30
30
  ok: true;
31
31
  status: "SUCCEEDED";
32
32
  moduleRef: "agent-gateway";
33
- moduleVersion: "0.1.17";
33
+ moduleVersion: "0.1.19";
34
34
  data: {
35
35
  setupStatus: "BLOCKED" | "READY";
36
36
  runtimeStatus: "RUNNING" | "STOPPED";
@@ -51,7 +51,7 @@ export declare const behaviorAdapter: {
51
51
  ok: true;
52
52
  status: "SUCCEEDED";
53
53
  moduleRef: "agent-gateway";
54
- moduleVersion: "0.1.17";
54
+ moduleVersion: "0.1.19";
55
55
  data: {
56
56
  localBaseUrl: string;
57
57
  publicBaseUrl?: string;
@@ -66,7 +66,7 @@ export declare const behaviorAdapter: {
66
66
  ok: true;
67
67
  status: "SUCCEEDED";
68
68
  moduleRef: "agent-gateway";
69
- moduleVersion: "0.1.17";
69
+ moduleVersion: "0.1.19";
70
70
  data: {
71
71
  docs: string;
72
72
  };
@@ -79,7 +79,7 @@ export declare const behaviorAdapter: {
79
79
  ok: true;
80
80
  status: "SUCCEEDED";
81
81
  moduleRef: "agent-gateway";
82
- moduleVersion: "0.1.17";
82
+ moduleVersion: "0.1.19";
83
83
  data: {
84
84
  host: string;
85
85
  port: number;
@@ -90,7 +90,7 @@ export declare const behaviorAdapter: {
90
90
  result: {
91
91
  contract: "deployment.result.v1";
92
92
  moduleRef: "agent-gateway";
93
- moduleVersion: "0.1.17";
93
+ moduleVersion: "0.1.19";
94
94
  ok: false;
95
95
  status: "FAILED";
96
96
  error: {
@@ -107,7 +107,7 @@ export declare const behaviorAdapter: {
107
107
  readonly ok: true;
108
108
  readonly status: "SUCCEEDED";
109
109
  readonly moduleRef: "agent-gateway";
110
- readonly moduleVersion: "0.1.17";
110
+ readonly moduleVersion: "0.1.19";
111
111
  };
112
112
  observedEffects: string[];
113
113
  } | {
@@ -116,11 +116,11 @@ export declare const behaviorAdapter: {
116
116
  readonly ok: true;
117
117
  readonly status: "SUCCEEDED";
118
118
  readonly moduleRef: "agent-gateway";
119
- readonly moduleVersion: "0.1.17";
119
+ readonly moduleVersion: "0.1.19";
120
120
  } | {
121
121
  contract: "deployment.result.v1";
122
122
  moduleRef: "agent-gateway";
123
- moduleVersion: "0.1.17";
123
+ moduleVersion: "0.1.19";
124
124
  ok: false;
125
125
  status: "FAILED";
126
126
  error: {
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { appendFile, mkdir, stat, rename } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
3
4
  import { deterministicLoopbackPort, readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
4
5
  import { descriptor } from "./descriptor.js";
5
6
  const services = new Map();
@@ -72,6 +73,36 @@ async function compose(context) {
72
73
  const credentialFile = join(deps.stateRoot, "agent", "secrets", "role-credentials.json");
73
74
  if (!existsSync(credentialFile))
74
75
  throw new Error("Agent role credential store is not materialized by Platform Host");
76
+ const logPath = join(deps.stateRoot, "logs", "agent-gateway", "events.jsonl");
77
+ let logTail = Promise.resolve();
78
+ let pendingLogs = 0;
79
+ const log = (entry) => {
80
+ if (pendingLogs >= 1000)
81
+ return;
82
+ pendingLogs++;
83
+ logTail = logTail
84
+ .catch(() => undefined)
85
+ .then(async () => {
86
+ await mkdir(dirname(logPath), { recursive: true, mode: 0o700 });
87
+ const target = entry.component === "agent-gateway-process"
88
+ ? logPath.replace("events.jsonl", "lifecycle.jsonl")
89
+ : logPath;
90
+ const info = await stat(target).catch(() => null);
91
+ if (info &&
92
+ (info.size >= 5 * 1024 * 1024 ||
93
+ Date.now() - info.mtimeMs > 7 * 86400_000))
94
+ await rename(target, `${target}.1`);
95
+ await appendFile(target, `${JSON.stringify(entry)}\n`, {
96
+ encoding: "utf8",
97
+ mode: 0o600,
98
+ });
99
+ })
100
+ .catch(() => undefined)
101
+ .finally(() => {
102
+ pendingLogs--;
103
+ });
104
+ void logTail;
105
+ };
75
106
  const { createAgentGatewayProcess, parseAgentGatewayProcessConfig } = await import("../src/process.js");
76
107
  const listener = new URL(own.localBaseUrl);
77
108
  return createAgentGatewayProcess({
@@ -83,6 +114,8 @@ async function compose(context) {
83
114
  credentialFile,
84
115
  downstreamCredentialFile: deps.downstreamCredentialFile,
85
116
  }),
117
+ log,
118
+ operationLog: log,
86
119
  });
87
120
  }
88
121
  const failed = (code, message) => ({
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "agent-gateway";
5
5
  readonly packageName: "@tomflow/proflow-agent-gateway";
6
- readonly moduleVersion: "0.1.17";
6
+ readonly moduleVersion: "0.1.19";
7
7
  readonly kind: "service";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -3,7 +3,7 @@ export const descriptor = {
3
3
  contractVersion: "1.0.0",
4
4
  moduleRef: "agent-gateway",
5
5
  packageName: "@tomflow/proflow-agent-gateway",
6
- moduleVersion: "0.1.17",
6
+ moduleVersion: "0.1.19",
7
7
  kind: "service",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -22,6 +22,11 @@ export type GatewayOptions = {
22
22
  port?: number;
23
23
  now?: () => number;
24
24
  actionTimeoutMs?: number;
25
+ onIngressFailure?: (entry: {
26
+ status: "FAILED";
27
+ httpStatus: number;
28
+ sideEffectState: "NOT_APPLIED";
29
+ }) => void;
25
30
  };
26
31
  export declare class AgentGatewayError extends Error {
27
32
  readonly code: string;
package/dist/src/index.js CHANGED
@@ -225,6 +225,7 @@ export async function createAgentGateway(options) {
225
225
  };
226
226
  const handler = async (request, response) => {
227
227
  inFlight += 1;
228
+ let ownerEntered = false;
228
229
  try {
229
230
  const url = new URL(request.url ?? "/", `http://${host}`);
230
231
  if (request.method === "GET" && url.pathname === "/health") {
@@ -335,6 +336,7 @@ export async function createAgentGateway(options) {
335
336
  const propagationReserveMs = Math.min(1_500, Math.max(100, Math.floor(actionTimeoutMs / 10)));
336
337
  const deadlineAt = new Date(now() + Math.max(1, actionTimeoutMs - propagationReserveMs)).toISOString();
337
338
  const actionSignal = AbortSignal.timeout(actionTimeoutMs);
339
+ ownerEntered = true;
338
340
  const operation = action.uncertain && options.owners.lookupResult
339
341
  ? options.owners.lookupResult(action.operationId, authenticatedRoleRef, canonicalBody)
340
342
  : options.owners.route(action.operationId, authenticatedRoleRef, canonicalBody, {
@@ -383,6 +385,16 @@ export async function createAgentGateway(options) {
383
385
  }));
384
386
  }
385
387
  finally {
388
+ if (!ownerEntered && response.statusCode >= 400) {
389
+ try {
390
+ options.onIngressFailure?.({
391
+ status: "FAILED",
392
+ httpStatus: response.statusCode,
393
+ sideEffectState: "NOT_APPLIED",
394
+ });
395
+ }
396
+ catch { }
397
+ }
386
398
  inFlight -= 1;
387
399
  }
388
400
  };
@@ -12,6 +12,7 @@ export declare function createAgentGatewayProcess(input: {
12
12
  config: AgentGatewayProcessConfig;
13
13
  fetch?: typeof globalThis.fetch;
14
14
  log?: (entry: Record<string, unknown>) => void;
15
+ operationLog?: (entry: Record<string, unknown>) => void;
15
16
  }): Promise<Readonly<{
16
17
  readiness: () => Promise<{
17
18
  status: "NOT_READY" | "READY";
@@ -1,4 +1,4 @@
1
- import { createHash, timingSafeEqual } from "node:crypto";
1
+ import { createHash, timingSafeEqual, randomUUID } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import { AgentGatewayError, createAgentGateway } from "./index.js";
@@ -31,6 +31,7 @@ const safeDownstreamErrorCodes = new Set([
31
31
  "LOCAL_TOOL_SCOPE_DENIED",
32
32
  "LOCAL_TOOL_COMMAND_FAILED",
33
33
  ]);
34
+ const directToolOperationIds = new Set(["localDev", "repomix", "codeGraph"]);
34
35
  async function boundedDownstreamErrorCode(response) {
35
36
  if (!response.body)
36
37
  return null;
@@ -82,6 +83,25 @@ function text(value, name) {
82
83
  throw new TypeError(`${name} must be a non-empty string`);
83
84
  return value;
84
85
  }
86
+ function logErrorCode(error) {
87
+ const value = error instanceof AgentGatewayError
88
+ ? error.code
89
+ : error &&
90
+ typeof error === "object" &&
91
+ typeof Reflect.get(error, "message") === "string"
92
+ ? String(Reflect.get(error, "message"))
93
+ : "GATEWAY_FAILURE";
94
+ return /^[A-Z][A-Z0-9_.:-]{0,159}$/.test(value) ? value : "GATEWAY_FAILURE";
95
+ }
96
+ function optionalToolOperation(value) {
97
+ if (typeof value !== "object" ||
98
+ value === null ||
99
+ Array.isArray(value) ||
100
+ typeof Reflect.get(value, "operation") !== "string")
101
+ return undefined;
102
+ const operation = String(Reflect.get(value, "operation"));
103
+ return /^[A-Za-z0-9_.:-]{1,160}$/.test(operation) ? operation : undefined;
104
+ }
85
105
  export function parseAgentGatewayProcessConfig(value) {
86
106
  const input = record(value, "agent-gateway config");
87
107
  const publicBaseUrl = new URL(text(input.publicBaseUrl, "publicBaseUrl"));
@@ -128,6 +148,9 @@ function parseCredentialStore(value) {
128
148
  return credentials;
129
149
  }
130
150
  async function readCurrentCredentialStore(file) {
151
+ const info = await stat(file);
152
+ if (process.platform !== "win32" && (info.mode & 0o077) !== 0)
153
+ throw new Error("ROLE_CREDENTIAL_STORE_PERMISSIONS_INVALID");
131
154
  return parseCredentialStore(JSON.parse(await readFile(file, "utf8")));
132
155
  }
133
156
  async function readDownstreamCredential(file) {
@@ -142,11 +165,6 @@ async function readDownstreamCredential(file) {
142
165
  export async function createAgentGatewayProcess(input) {
143
166
  const fetchImplementation = input.fetch ?? globalThis.fetch;
144
167
  const credentialFile = input.config.credentialFile;
145
- // Fail-fast at startup so a malformed configured credential store is rejected
146
- // before the process advertises readiness. Authentication re-reads the current
147
- // store on every attempt below, so a rotated key takes effect without a restart
148
- // and a malformed/half-written store fails closed instead of serving a stale
149
- // snapshot.
150
168
  await readCurrentCredentialStore(credentialFile);
151
169
  if (input.config.downstreamCredentialFile)
152
170
  await readDownstreamCredential(input.config.downstreamCredentialFile);
@@ -158,7 +176,7 @@ export async function createAgentGatewayProcess(input) {
158
176
  return false;
159
177
  }
160
178
  };
161
- const downstream = async (path, body, signal) => {
179
+ const downstream = async (path, body, signal, operationRef) => {
162
180
  const downstreamCredential = input.config.downstreamCredentialFile
163
181
  ? await readDownstreamCredential(input.config.downstreamCredentialFile)
164
182
  : undefined;
@@ -167,6 +185,9 @@ export async function createAgentGatewayProcess(input) {
167
185
  response = await fetchImplementation(`${input.config.downstreamBaseUrl}${path}`, {
168
186
  method: body === undefined ? "GET" : "POST",
169
187
  headers: {
188
+ ...(operationRef
189
+ ? { "x-proflow-operation-ref": operationRef }
190
+ : {}),
170
191
  ...(body === undefined
171
192
  ? {}
172
193
  : { "content-type": "application/json" }),
@@ -185,21 +206,69 @@ export async function createAgentGatewayProcess(input) {
185
206
  }
186
207
  if (!response.ok) {
187
208
  const downstreamCode = await boundedDownstreamErrorCode(response);
188
- const code = response.status >= 500 || response.status === 401
209
+ const code = response.status === 401
189
210
  ? "OWNER_SERVICE_UNAVAILABLE"
190
211
  : (downstreamCode ??
191
- (response.status === 400
192
- ? "INVALID_REQUEST"
193
- : response.status === 403
194
- ? "ROLE_OPERATION_DENIED"
195
- : "DOWNSTREAM_UNAVAILABLE"));
212
+ (response.status >= 500
213
+ ? "OWNER_SERVICE_UNAVAILABLE"
214
+ : response.status === 400
215
+ ? "INVALID_REQUEST"
216
+ : response.status === 403
217
+ ? "ROLE_OPERATION_DENIED"
218
+ : "DOWNSTREAM_UNAVAILABLE"));
196
219
  throw Object.assign(new AgentGatewayError(code), {
197
220
  httpStatus: response.status,
198
221
  });
199
222
  }
200
223
  return response.json();
201
224
  };
225
+ const emitOperation = (inputEvent) => {
226
+ const toolOperation = directToolOperationIds.has(inputEvent.operationId)
227
+ ? optionalToolOperation(inputEvent.value)
228
+ : undefined;
229
+ try {
230
+ void Promise.resolve(input.operationLog?.({
231
+ contract: "proflow.operation-boundary.v1",
232
+ eventId: `gateway-event:${randomUUID()}`,
233
+ operationRef: inputEvent.operationRef,
234
+ correlationKind: "EXACT",
235
+ timestamp: new Date().toISOString(),
236
+ source: "agent-gateway",
237
+ component: "agent-gateway-ingress",
238
+ event: inputEvent.mode === "LOOKUP"
239
+ ? "GATEWAY_ACTION_LOOKUP"
240
+ : "GATEWAY_ACTION",
241
+ boundary: directToolOperationIds.has(inputEvent.operationId)
242
+ ? "TOOL_INVOCATION"
243
+ : "ACTION",
244
+ status: inputEvent.status,
245
+ operationId: inputEvent.operationId,
246
+ roleRef: inputEvent.roleRef,
247
+ ...(toolOperation ? { toolOperation } : {}),
248
+ ...(inputEvent.errorCode ? { errorCode: inputEvent.errorCode } : {}),
249
+ durationMs: inputEvent.durationMs,
250
+ })).catch(() => undefined);
251
+ }
252
+ catch { }
253
+ };
202
254
  const gateway = await createAgentGateway({
255
+ onIngressFailure(entry) {
256
+ try {
257
+ void Promise.resolve(input.operationLog?.({
258
+ contract: "proflow.operation-boundary.v1",
259
+ timestamp: new Date().toISOString(),
260
+ eventId: `gateway-event:${randomUUID()}`,
261
+ operationRef: `op:${randomUUID()}`,
262
+ correlationKind: "EXACT",
263
+ source: "agent-gateway",
264
+ component: "agent-gateway-ingress",
265
+ event: "GATEWAY_INGRESS_REJECTED",
266
+ ...entry,
267
+ errorCode: "GATEWAY_INGRESS_REJECTED",
268
+ })).catch(() => undefined);
269
+ }
270
+ catch { }
271
+ },
203
272
  host: input.config.host,
204
273
  port: input.config.port,
205
274
  relayBaseUrl: `${input.config.publicBaseUrl}/relay/`,
@@ -211,23 +280,74 @@ export async function createAgentGatewayProcess(input) {
211
280
  return roleRef;
212
281
  throw new Error("AUTHENTICATION_FAILED");
213
282
  },
214
- route(operationId, authenticatedRoleRef, value, context) {
215
- return downstream(`/actions/${encodeURIComponent(operationId)}`, {
216
- authenticatedRoleRef,
217
- input: value,
218
- deadlineAt: context?.deadlineAt,
219
- ...(context?.fileMaterializationInputs === undefined
220
- ? {}
221
- : {
222
- fileMaterializationInputs: context.fileMaterializationInputs,
223
- }),
224
- }, context?.signal);
283
+ async route(operationId, authenticatedRoleRef, value, context) {
284
+ const started = performance.now();
285
+ const operationRef = `op:${randomUUID()}`;
286
+ try {
287
+ const result = await downstream(`/actions/${encodeURIComponent(operationId)}`, {
288
+ authenticatedRoleRef,
289
+ input: value,
290
+ deadlineAt: context?.deadlineAt,
291
+ ...(context?.fileMaterializationInputs === undefined
292
+ ? {}
293
+ : {
294
+ fileMaterializationInputs: context.fileMaterializationInputs,
295
+ }),
296
+ }, context?.signal, operationRef);
297
+ emitOperation({
298
+ mode: "ROUTE",
299
+ operationRef,
300
+ operationId,
301
+ roleRef: authenticatedRoleRef,
302
+ value,
303
+ status: "SUCCEEDED",
304
+ durationMs: performance.now() - started,
305
+ });
306
+ return result;
307
+ }
308
+ catch (error) {
309
+ emitOperation({
310
+ mode: "ROUTE",
311
+ operationRef,
312
+ operationId,
313
+ roleRef: authenticatedRoleRef,
314
+ value,
315
+ status: "FAILED",
316
+ errorCode: logErrorCode(error),
317
+ durationMs: performance.now() - started,
318
+ });
319
+ throw error;
320
+ }
225
321
  },
226
- lookupResult(operationId, authenticatedRoleRef, value) {
227
- return downstream(`/actions/${encodeURIComponent(operationId)}/result`, {
228
- authenticatedRoleRef,
229
- input: value,
230
- });
322
+ async lookupResult(operationId, authenticatedRoleRef, value) {
323
+ const started = performance.now();
324
+ const operationRef = `op:${randomUUID()}`;
325
+ try {
326
+ const result = await downstream(`/actions/${encodeURIComponent(operationId)}/result`, { authenticatedRoleRef, input: value }, undefined, operationRef);
327
+ emitOperation({
328
+ mode: "LOOKUP",
329
+ operationRef,
330
+ operationId,
331
+ roleRef: authenticatedRoleRef,
332
+ value,
333
+ status: "SUCCEEDED",
334
+ durationMs: performance.now() - started,
335
+ });
336
+ return result;
337
+ }
338
+ catch (error) {
339
+ emitOperation({
340
+ mode: "LOOKUP",
341
+ operationRef,
342
+ operationId,
343
+ roleRef: authenticatedRoleRef,
344
+ value,
345
+ status: "FAILED",
346
+ errorCode: logErrorCode(error),
347
+ durationMs: performance.now() - started,
348
+ });
349
+ throw error;
350
+ }
231
351
  },
232
352
  async readiness() {
233
353
  try {
@@ -260,6 +380,7 @@ export async function createAgentGatewayProcess(input) {
260
380
  timestamp: new Date().toISOString(),
261
381
  component: "agent-gateway-process",
262
382
  event: "SERVICE_STARTED",
383
+ status: "SUCCEEDED",
263
384
  ...address,
264
385
  });
265
386
  return address;
@@ -270,6 +391,7 @@ export async function createAgentGatewayProcess(input) {
270
391
  timestamp: new Date().toISOString(),
271
392
  component: "agent-gateway-process",
272
393
  event: "SERVICE_STOPPED",
394
+ status: "SUCCEEDED",
273
395
  });
274
396
  };
275
397
  return Object.freeze({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-agent-gateway",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -28,14 +28,14 @@
28
28
  "@tomflow/proflow-module-contract": "^0.1.13"
29
29
  },
30
30
  "devDependencies": {
31
- "@tomflow/proflow-execution-browser-extension": "^0.1.51",
31
+ "@tomflow/proflow-task-migration-runner": "^0.1.11",
32
+ "@tomflow/proflow-execution-browser-extension": "^0.1.62",
32
33
  "@tomflow/proflow-deployment-conformance": "^0.1.13",
34
+ "@tomflow/proflow-agent-runtime": "^0.1.17",
35
+ "@tomflow/proflow-task-store-sqlite": "^0.1.12",
33
36
  "@tomflow/proflow-execution-runtime": "^0.1.19",
34
- "@tomflow/proflow-agent-runtime": "^0.1.14",
35
- "@tomflow/proflow-platform-host": "^0.1.23",
36
- "@tomflow/proflow-task-orchestration": "^0.1.11",
37
- "@tomflow/proflow-task-migration-runner": "^0.1.11",
38
- "@tomflow/proflow-task-store-sqlite": "^0.1.12"
37
+ "@tomflow/proflow-platform-host": "^0.1.29",
38
+ "@tomflow/proflow-task-orchestration": "^0.1.11"
39
39
  },
40
40
  "description": "The sole Custom GPT Actions HTTP ingress and OpenAI transport anti-corruption layer.",
41
41
  "keywords": [
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "agent-gateway",
5
5
  "packageName": "@tomflow/proflow-agent-gateway",
6
- "moduleVersion": "0.1.17",
6
+ "moduleVersion": "0.1.19",
7
7
  "kind": "service",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",