forgeos 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
Files changed (180) hide show
  1. package/AGENTS.md +38 -3
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +25 -10
  4. package/package.json +8 -5
  5. package/src/forge/_generated/actionSubscriptions.json +2 -2
  6. package/src/forge/_generated/actionSubscriptions.ts +3 -3
  7. package/src/forge/_generated/agentAdapterManifest.json +2 -2
  8. package/src/forge/_generated/agentAdapterManifest.ts +3 -3
  9. package/src/forge/_generated/agentContract.json +2 -2
  10. package/src/forge/_generated/agentContract.ts +183 -50
  11. package/src/forge/_generated/agentQuickstart.md +3 -1
  12. package/src/forge/_generated/agentTools.json +2 -0
  13. package/src/forge/_generated/agentTools.md +16 -0
  14. package/src/forge/_generated/agentTools.ts +12 -0
  15. package/src/forge/_generated/aiContext.ts +67 -1
  16. package/src/forge/_generated/aiModels.json +2 -2
  17. package/src/forge/_generated/aiModels.ts +17 -1
  18. package/src/forge/_generated/aiProviders.json +1 -1
  19. package/src/forge/_generated/aiProviders.ts +1 -1
  20. package/src/forge/_generated/aiRegistry.json +2 -2
  21. package/src/forge/_generated/aiRegistry.ts +7 -5
  22. package/src/forge/_generated/api.json +2 -2
  23. package/src/forge/_generated/api.ts +1 -1
  24. package/src/forge/_generated/appGraph.json +2 -2
  25. package/src/forge/_generated/appGraph.ts +512 -260
  26. package/src/forge/_generated/appMap.md +21 -1
  27. package/src/forge/_generated/artifactManifest.json +2 -2
  28. package/src/forge/_generated/artifactManifest.ts +2 -2
  29. package/src/forge/_generated/authClaims.json +1 -1
  30. package/src/forge/_generated/authClaims.ts +1 -1
  31. package/src/forge/_generated/authConfig.json +1 -1
  32. package/src/forge/_generated/authConfig.ts +1 -1
  33. package/src/forge/_generated/authContext.ts +1 -1
  34. package/src/forge/_generated/authRegistry.json +1 -1
  35. package/src/forge/_generated/authRegistry.ts +1 -1
  36. package/src/forge/_generated/buildInfo.json +2 -2
  37. package/src/forge/_generated/buildInfo.ts +4 -4
  38. package/src/forge/_generated/capabilityMap.json +2 -2
  39. package/src/forge/_generated/capabilityMap.md +1 -1
  40. package/src/forge/_generated/capabilityMap.ts +2 -2
  41. package/src/forge/_generated/client.ts +1 -1
  42. package/src/forge/_generated/clientApi.ts +1 -1
  43. package/src/forge/_generated/clientManifest.json +2 -2
  44. package/src/forge/_generated/clientManifest.ts +3 -3
  45. package/src/forge/_generated/clientTypes.ts +1 -1
  46. package/src/forge/_generated/configRegistry.json +1 -1
  47. package/src/forge/_generated/configRegistry.ts +1 -1
  48. package/src/forge/_generated/dataGraph.json +2 -2
  49. package/src/forge/_generated/dataGraph.ts +3 -3
  50. package/src/forge/_generated/db.json +1 -1
  51. package/src/forge/_generated/db.ts +1 -1
  52. package/src/forge/_generated/dbSecurityManifest.json +1 -1
  53. package/src/forge/_generated/dbSecurityManifest.ts +1 -1
  54. package/src/forge/_generated/dbSessionContext.json +1 -1
  55. package/src/forge/_generated/dbSessionContext.ts +1 -1
  56. package/src/forge/_generated/deployManifest.json +2 -2
  57. package/src/forge/_generated/deployManifest.ts +7 -7
  58. package/src/forge/_generated/devManifest.json +2 -2
  59. package/src/forge/_generated/devManifest.ts +18 -3
  60. package/src/forge/_generated/envSchema.json +1 -1
  61. package/src/forge/_generated/envSchema.ts +1 -1
  62. package/src/forge/_generated/frontendGraph.json +1 -1
  63. package/src/forge/_generated/frontendGraph.ts +1 -1
  64. package/src/forge/_generated/importGuards.json +1 -1
  65. package/src/forge/_generated/importGuards.ts +1 -1
  66. package/src/forge/_generated/index.ts +2 -1
  67. package/src/forge/_generated/liveProductionManifest.json +1 -1
  68. package/src/forge/_generated/liveProductionManifest.ts +1 -1
  69. package/src/forge/_generated/liveProtocol.json +1 -1
  70. package/src/forge/_generated/liveProtocol.ts +1 -1
  71. package/src/forge/_generated/liveQueryRegistry.json +2 -2
  72. package/src/forge/_generated/liveQueryRegistry.ts +3 -3
  73. package/src/forge/_generated/liveTransportConfig.json +1 -1
  74. package/src/forge/_generated/liveTransportConfig.ts +1 -1
  75. package/src/forge/_generated/makeRegistry.json +2 -2
  76. package/src/forge/_generated/makeRegistry.ts +16 -2
  77. package/src/forge/_generated/makeTemplates.json +2 -2
  78. package/src/forge/_generated/makeTemplates.ts +6 -1
  79. package/src/forge/_generated/mockMap.json +1 -1
  80. package/src/forge/_generated/mockMap.ts +1 -1
  81. package/src/forge/_generated/operationPlaybooks.md +34 -14
  82. package/src/forge/_generated/packageGraph.json +2 -2
  83. package/src/forge/_generated/packageGraph.ts +8808 -4723
  84. package/src/forge/_generated/packageUpgradeRegistry.json +2 -2
  85. package/src/forge/_generated/packageUpgradeRegistry.ts +2 -2
  86. package/src/forge/_generated/permissionMatrix.json +2 -2
  87. package/src/forge/_generated/permissionMatrix.ts +3 -3
  88. package/src/forge/_generated/policyRegistry.json +2 -2
  89. package/src/forge/_generated/policyRegistry.ts +3 -3
  90. package/src/forge/_generated/queryRegistry.json +2 -2
  91. package/src/forge/_generated/queryRegistry.ts +3 -3
  92. package/src/forge/_generated/react.d.ts +1 -1
  93. package/src/forge/_generated/react.ts +1 -1
  94. package/src/forge/_generated/reactManifest.json +2 -2
  95. package/src/forge/_generated/reactManifest.ts +3 -3
  96. package/src/forge/_generated/releaseManifest.json +2 -2
  97. package/src/forge/_generated/releaseManifest.ts +3 -3
  98. package/src/forge/_generated/rlsPolicies.json +1 -1
  99. package/src/forge/_generated/rlsPolicies.sql +1 -1
  100. package/src/forge/_generated/rlsPolicies.ts +1 -1
  101. package/src/forge/_generated/runtimeGraph.json +2 -2
  102. package/src/forge/_generated/runtimeGraph.ts +3 -3
  103. package/src/forge/_generated/runtimeMatrix.json +2 -2
  104. package/src/forge/_generated/runtimeMatrix.ts +8684 -1939
  105. package/src/forge/_generated/runtimeRegistry.ts +1 -1
  106. package/src/forge/_generated/runtimeRules.md +13 -1
  107. package/src/forge/_generated/secretRegistry.json +1 -1
  108. package/src/forge/_generated/secretRegistry.ts +1 -1
  109. package/src/forge/_generated/secretsContext.ts +1 -1
  110. package/src/forge/_generated/serverApi.ts +1 -1
  111. package/src/forge/_generated/sourceMapManifest.json +2 -2
  112. package/src/forge/_generated/sourceMapManifest.ts +2 -2
  113. package/src/forge/_generated/sqlPlan.json +1 -1
  114. package/src/forge/_generated/sqlPlan.ts +1 -1
  115. package/src/forge/_generated/subscriptionManifest.json +2 -2
  116. package/src/forge/_generated/subscriptionManifest.ts +3 -3
  117. package/src/forge/_generated/symbolicationManifest.json +2 -2
  118. package/src/forge/_generated/symbolicationManifest.ts +2 -2
  119. package/src/forge/_generated/telemetryRegistry.json +2 -2
  120. package/src/forge/_generated/telemetryRegistry.ts +3 -3
  121. package/src/forge/_generated/telemetrySinks.json +2 -2
  122. package/src/forge/_generated/telemetrySinks.ts +2 -2
  123. package/src/forge/_generated/tenantScope.json +2 -2
  124. package/src/forge/_generated/tenantScope.ts +3 -3
  125. package/src/forge/_generated/testGraph.json +2 -2
  126. package/src/forge/_generated/testGraph.ts +339 -17
  127. package/src/forge/_generated/testPlanRegistry.json +2 -2
  128. package/src/forge/_generated/testPlanRegistry.ts +2 -2
  129. package/src/forge/_generated/uiRoutes.json +1 -1
  130. package/src/forge/_generated/uiRoutes.ts +1 -1
  131. package/src/forge/_generated/uiScenarios.json +1 -1
  132. package/src/forge/_generated/uiScenarios.ts +1 -1
  133. package/src/forge/_generated/uiTestManifest.json +2 -2
  134. package/src/forge/_generated/uiTestManifest.ts +2 -2
  135. package/src/forge/_generated/workflowRegistry.json +2 -2
  136. package/src/forge/_generated/workflowRegistry.ts +3 -3
  137. package/src/forge/_generated/workflowSubscriptions.json +2 -2
  138. package/src/forge/_generated/workflowSubscriptions.ts +3 -3
  139. package/src/forge/cli/ai.ts +351 -1
  140. package/src/forge/cli/auth.ts +36 -1
  141. package/src/forge/cli/commands.ts +19 -0
  142. package/src/forge/cli/parse.ts +67 -8
  143. package/src/forge/cli/rls.ts +529 -17
  144. package/src/forge/cli/secrets.ts +46 -1
  145. package/src/forge/cli/security.ts +269 -0
  146. package/src/forge/compiler/agent-contract/build.ts +289 -8
  147. package/src/forge/compiler/agent-contract/types.ts +43 -0
  148. package/src/forge/compiler/ai-registry/build.ts +62 -1
  149. package/src/forge/compiler/ai-registry/constants.ts +1 -1
  150. package/src/forge/compiler/ai-registry/parse.ts +98 -4
  151. package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
  152. package/src/forge/compiler/dev-manifest/build.ts +3 -0
  153. package/src/forge/compiler/diagnostics/codes.ts +15 -0
  154. package/src/forge/compiler/diagnostics/create.ts +1 -1
  155. package/src/forge/compiler/make-registry/build.ts +13 -0
  156. package/src/forge/compiler/orchestrator/plan.ts +11 -0
  157. package/src/forge/compiler/orchestrator/serialize.ts +68 -0
  158. package/src/forge/compiler/package-graph/compiler.ts +13 -3
  159. package/src/forge/compiler/types/ai-registry.ts +25 -1
  160. package/src/forge/compiler/types/app-graph.ts +1 -0
  161. package/src/forge/compiler/types/cli.ts +1 -0
  162. package/src/forge/compiler/types/dev-manifest.ts +3 -0
  163. package/src/forge/dev/server.ts +508 -1
  164. package/src/forge/make/index.ts +126 -3
  165. package/src/forge/make/templates.ts +188 -0
  166. package/src/forge/make/types.ts +1 -0
  167. package/src/forge/runtime/ai/context.ts +210 -5
  168. package/src/forge/runtime/ai/types.ts +70 -0
  169. package/src/forge/runtime/auth/claims.ts +32 -0
  170. package/src/forge/runtime/auth/errors.ts +2 -0
  171. package/src/forge/runtime/context/create-context.ts +30 -6
  172. package/src/forge/runtime/db/memory-adapter.ts +2 -2
  173. package/src/forge/runtime/telemetry/scrubber.ts +56 -5
  174. package/src/forge/runtime/webhooks/security.ts +184 -0
  175. package/src/forge/server.ts +93 -0
  176. package/src/forge/version.ts +1 -1
  177. package/templates/b2b-support-web/package.json +1 -0
  178. package/templates/b2b-support-web/tsconfig.json +4 -1
  179. package/templates/minimal-web/package.json +1 -0
  180. package/templates/minimal-web/tsconfig.json +3 -1
@@ -51,10 +51,80 @@ export interface ForgeGenerateStructuredInput<T> {
51
51
  schema: ForgeFlexibleSchema<T>;
52
52
  }
53
53
 
54
+ export type ForgeAiToolRisk = "read" | "write" | "external" | "destructive";
55
+
56
+ export interface ForgeAiToolRuntimeContext {
57
+ secrets: {
58
+ get(name: string): string;
59
+ optional(name: string): string | undefined;
60
+ has(name: string): boolean;
61
+ };
62
+ env: Record<string, string | undefined>;
63
+ telemetry?: {
64
+ traceId?: string;
65
+ capture(name: string, properties?: Record<string, unknown>): Promise<void>;
66
+ };
67
+ auth?: unknown;
68
+ }
69
+
70
+ export interface ForgeAiToolDefinition<TArgs = unknown, TResult = unknown> {
71
+ description: string;
72
+ inputSchema: unknown;
73
+ outputSchema?: unknown;
74
+ strict?: boolean;
75
+ needsApproval?: boolean | ((args: TArgs) => boolean | Promise<boolean>);
76
+ risk?: ForgeAiToolRisk;
77
+ handler: (
78
+ ctx: ForgeAiToolRuntimeContext,
79
+ args: TArgs,
80
+ ) => TResult | Promise<TResult>;
81
+ }
82
+
83
+ export type ForgeAgentStopWhen =
84
+ | { kind: "stepCount"; maxSteps: number }
85
+ | { kind: "toolCall"; toolName: string };
86
+
87
+ export interface ForgeRunAgentInput {
88
+ provider?: ForgeAiProvider;
89
+ model: string;
90
+ prompt: string;
91
+ instructions: string;
92
+ purpose?: string;
93
+ tools?: Record<string, ForgeAiToolDefinition>;
94
+ stopWhen?: ForgeAgentStopWhen;
95
+ maxSteps?: number;
96
+ temperature?: number;
97
+ maxTokens?: number;
98
+ }
99
+
100
+ export interface ForgeRunAgentResult {
101
+ text: string;
102
+ provider: ForgeAiProvider;
103
+ model: string;
104
+ purpose?: string;
105
+ usage: ForgeAiUsage;
106
+ latencyMs: number;
107
+ toolCalls: Array<{
108
+ toolName: string;
109
+ input: unknown;
110
+ }>;
111
+ toolResults: Array<{
112
+ toolName: string;
113
+ output: unknown;
114
+ }>;
115
+ steps: number;
116
+ estimatedCostUsd?: number;
117
+ }
118
+
54
119
  export interface AiContext {
55
120
  generateText(input: ForgeGenerateTextInput): Promise<ForgeGenerateTextResult>;
56
121
  streamText(input: ForgeStreamTextInput): Promise<ForgeStreamTextResult>;
57
122
  generateStructured<T>(input: ForgeGenerateStructuredInput<T>): Promise<T>;
123
+ runAgent(input: ForgeRunAgentInput): Promise<ForgeRunAgentResult>;
124
+ }
125
+
126
+ export interface AgentRuntimeContext {
127
+ run(input: ForgeRunAgentInput): Promise<ForgeRunAgentResult>;
58
128
  }
59
129
 
60
130
  export interface AiTelemetryEnvelope {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ FORGE_AUTH_CLAIM_INVALID,
2
3
  FORGE_AUTH_CLAIM_MISSING,
3
4
  FORGE_AUTH_TENANT_MISSING,
4
5
  } from "../../compiler/diagnostics/codes.ts";
@@ -57,6 +58,31 @@ function asStringArray(value: unknown): string[] {
57
58
  return [];
58
59
  }
59
60
 
61
+ function claimExists(claims: Record<string, unknown>, path: string | undefined): boolean {
62
+ return getClaimValue(claims, path) !== undefined;
63
+ }
64
+
65
+ function validateRoleClaim(
66
+ claims: Record<string, unknown>,
67
+ path: string | undefined,
68
+ label: string,
69
+ ): void {
70
+ const value = getClaimValue(claims, path);
71
+ if (value === undefined) {
72
+ return;
73
+ }
74
+ if (typeof value === "string") {
75
+ return;
76
+ }
77
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
78
+ return;
79
+ }
80
+ throw new ForgeAuthError(
81
+ FORGE_AUTH_CLAIM_INVALID,
82
+ `auth claim '${path ?? label}' must be a string or string array`,
83
+ );
84
+ }
85
+
60
86
  function uniqueSorted(values: string[]): string[] {
61
87
  return [...new Set(values)].sort();
62
88
  }
@@ -84,6 +110,12 @@ export function mapClaimsToAuthContext(
84
110
  );
85
111
  }
86
112
 
113
+ if (claimExists(payload, mapping.role)) {
114
+ validateRoleClaim(payload, mapping.role, "role");
115
+ }
116
+ if (claimExists(payload, mapping.roles)) {
117
+ validateRoleClaim(payload, mapping.roles, "roles");
118
+ }
87
119
  const role = asString(getClaimValue(payload, mapping.role));
88
120
  const roles = uniqueSorted([
89
121
  ...(role ? [role] : []),
@@ -1,4 +1,5 @@
1
1
  import {
2
+ FORGE_AUTH_CLAIM_INVALID,
2
3
  FORGE_AUTH_CLAIM_MISSING,
3
4
  FORGE_AUTH_DEV_HEADERS_IN_PRODUCTION,
4
5
  FORGE_AUTH_DISABLED,
@@ -20,6 +21,7 @@ export type ForgeAuthDiagnosticCode =
20
21
  | typeof FORGE_AUTH_TOKEN_EXPIRED
21
22
  | typeof FORGE_AUTH_JWKS_FAILED
22
23
  | typeof FORGE_AUTH_CLAIM_MISSING
24
+ | typeof FORGE_AUTH_CLAIM_INVALID
23
25
  | typeof FORGE_AUTH_TENANT_MISSING
24
26
  | typeof FORGE_AUTH_DEV_HEADERS_IN_PRODUCTION
25
27
  | typeof FORGE_AUTH_MODE_INVALID
@@ -13,7 +13,7 @@ import { loadEnvFiles } from "../secrets/env-loader.ts";
13
13
  import { loadEnvSchema, loadSecretRegistry } from "../secrets/check.ts";
14
14
  import { createRuntimeSecretsBundle } from "../secrets/runtime-bundle.ts";
15
15
  import { createAiContext } from "../ai/context.ts";
16
- import type { AiContext } from "../ai/types.ts";
16
+ import type { AgentRuntimeContext, AiContext } from "../ai/types.ts";
17
17
  import { isMockAiEnabled } from "../ai/state.ts";
18
18
  import { currentReleaseInfo, type RuntimeReleaseInfo } from "../release/runtime.ts";
19
19
 
@@ -26,6 +26,7 @@ export interface ForgeContext {
26
26
  secrets: SecretsContext;
27
27
  config: ConfigContext;
28
28
  ai: AiContext;
29
+ agent: AgentRuntimeContext;
29
30
  release: RuntimeReleaseInfo;
30
31
  }
31
32
 
@@ -67,11 +68,16 @@ function buildSecretsConfigAndAi(
67
68
  runtimeKind: RuntimeContext,
68
69
  store: RuntimeEnvStore,
69
70
  telemetry: TelemetryContext,
70
- options?: { mockAi?: boolean },
71
+ options?: {
72
+ mockAi?: boolean;
73
+ auth?: AuthContext;
74
+ env?: Record<string, string | undefined>;
75
+ },
71
76
  ): {
72
77
  secrets: SecretsContext;
73
78
  config: ConfigContext;
74
79
  ai: AiContext;
80
+ agent: AgentRuntimeContext;
75
81
  env: Record<string, string | undefined>;
76
82
  } {
77
83
  const registry = workspaceRoot ? loadSecretRegistry(workspaceRoot) : null;
@@ -93,12 +99,20 @@ function buildSecretsConfigAndAi(
93
99
  tenantId:
94
100
  undefined,
95
101
  },
102
+ toolContext: {
103
+ env: options?.env ?? store.snapshot(),
104
+ auth: options?.auth,
105
+ },
96
106
  });
107
+ const agent: AgentRuntimeContext = {
108
+ run: (input) => ai.runAgent(input),
109
+ };
97
110
 
98
111
  return {
99
112
  secrets: bundle.secrets,
100
113
  config: bundle.config,
101
114
  ai,
115
+ agent,
102
116
  env: store.snapshot(),
103
117
  };
104
118
  }
@@ -123,12 +137,16 @@ export function createForgeContext(
123
137
  (options?.workspaceRoot
124
138
  ? getRuntimeEnvStore(options.workspaceRoot)
125
139
  : getRuntimeEnvStore());
126
- const { secrets, config, ai, env } = buildSecretsConfigAndAi(
140
+ const { secrets, config, ai, agent, env } = buildSecretsConfigAndAi(
127
141
  options?.workspaceRoot,
128
142
  runtimeKind,
129
143
  store,
130
144
  telemetry,
131
- { mockAi: options?.mockAi },
145
+ {
146
+ mockAi: options?.mockAi,
147
+ auth,
148
+ env: options?.env,
149
+ },
132
150
  );
133
151
 
134
152
  return {
@@ -139,6 +157,7 @@ export function createForgeContext(
139
157
  secrets,
140
158
  config,
141
159
  ai,
160
+ agent,
142
161
  release: currentReleaseInfo(),
143
162
  emit: async (eventType, payload) => {
144
163
  const enriched =
@@ -178,12 +197,16 @@ export function createActionContext(
178
197
  (options?.workspaceRoot
179
198
  ? getRuntimeEnvStore(options.workspaceRoot)
180
199
  : getRuntimeEnvStore());
181
- const { secrets, config, ai, env } = buildSecretsConfigAndAi(
200
+ const { secrets, config, ai, agent, env } = buildSecretsConfigAndAi(
182
201
  options?.workspaceRoot,
183
202
  runtimeKind,
184
203
  store,
185
204
  telemetry,
186
- { mockAi: options?.mockAi },
205
+ {
206
+ mockAi: options?.mockAi,
207
+ auth,
208
+ env: options?.env,
209
+ },
187
210
  );
188
211
 
189
212
  return {
@@ -194,6 +217,7 @@ export function createActionContext(
194
217
  secrets,
195
218
  config,
196
219
  ai,
220
+ agent,
197
221
  release: currentReleaseInfo(),
198
222
  emit: async () => {
199
223
  /* actions invoked by outbox worker do not emit */
@@ -656,9 +656,9 @@ export class MemoryAdapter implements DbAdapter {
656
656
  return { rows: [], rowCount: before - table.rows.length };
657
657
  }
658
658
 
659
- const id = params[0];
659
+ const rowsToDelete = new Set(this.filterRows(table.rows, sql, params));
660
660
  const before = table.rows.length;
661
- table.rows = table.rows.filter((row) => row.id !== id);
661
+ table.rows = table.rows.filter((row) => !rowsToDelete.has(row));
662
662
  return { rows: [], rowCount: before - table.rows.length };
663
663
  }
664
664
 
@@ -16,7 +16,51 @@ export interface ScrubResult<T> {
16
16
  diagnostics: Diagnostic[];
17
17
  }
18
18
 
19
- function redactValue(key: string, value: unknown, diagnostics: Diagnostic[]): unknown {
19
+ export interface ScrubOptions {
20
+ secretValues?: string[];
21
+ }
22
+
23
+ function normalizeSecretValues(secretValues: string[] | undefined): string[] {
24
+ return [...new Set((secretValues ?? []).filter((value) => value.length >= 8))]
25
+ .sort((left, right) => right.length - left.length);
26
+ }
27
+
28
+ function redactKnownSecretString(
29
+ key: string,
30
+ value: string,
31
+ diagnostics: Diagnostic[],
32
+ secretValues: string[],
33
+ ): string {
34
+ let redacted = value;
35
+ let didRedact = false;
36
+
37
+ for (const secret of secretValues) {
38
+ if (!redacted.includes(secret)) {
39
+ continue;
40
+ }
41
+ redacted = redacted.split(secret).join("[REDACTED]");
42
+ didRedact = true;
43
+ }
44
+
45
+ if (didRedact) {
46
+ diagnostics.push(
47
+ createDiagnostic({
48
+ severity: "warning",
49
+ code: FORGE_TELEMETRY_SECRET_REDACTED,
50
+ message: `redacted known secret value from telemetry field '${key}'`,
51
+ }),
52
+ );
53
+ }
54
+
55
+ return redacted;
56
+ }
57
+
58
+ function redactValue(
59
+ key: string,
60
+ value: unknown,
61
+ diagnostics: Diagnostic[],
62
+ secretValues: string[],
63
+ ): unknown {
20
64
  if (SECRET_KEY_PATTERN.test(key)) {
21
65
  diagnostics.push(
22
66
  createDiagnostic({
@@ -29,25 +73,30 @@ function redactValue(key: string, value: unknown, diagnostics: Diagnostic[]): un
29
73
  }
30
74
 
31
75
  if (value && typeof value === "object" && !Array.isArray(value)) {
32
- return scrubObject(value as Record<string, unknown>, diagnostics);
76
+ return scrubObject(value as Record<string, unknown>, diagnostics, secretValues);
33
77
  }
34
78
 
35
79
  if (Array.isArray(value)) {
36
80
  return value.map((item, index) =>
37
- redactValue(String(index), item, diagnostics),
81
+ redactValue(String(index), item, diagnostics, secretValues),
38
82
  );
39
83
  }
40
84
 
85
+ if (typeof value === "string" && secretValues.length > 0) {
86
+ return redactKnownSecretString(key, value, diagnostics, secretValues);
87
+ }
88
+
41
89
  return value;
42
90
  }
43
91
 
44
92
  function scrubObject(
45
93
  obj: Record<string, unknown>,
46
94
  diagnostics: Diagnostic[],
95
+ secretValues: string[],
47
96
  ): Record<string, unknown> {
48
97
  const result: Record<string, unknown> = {};
49
98
  for (const [key, value] of Object.entries(obj)) {
50
- result[key] = redactValue(key, value, diagnostics);
99
+ result[key] = redactValue(key, value, diagnostics, secretValues);
51
100
  }
52
101
  return result;
53
102
  }
@@ -80,9 +129,11 @@ function truncateString(
80
129
 
81
130
  export function scrubEnvelopePayload<T extends Record<string, unknown>>(
82
131
  payload: T,
132
+ options: ScrubOptions = {},
83
133
  ): ScrubResult<T> {
84
134
  const diagnostics: Diagnostic[] = [];
85
- const scrubbed = scrubObject(payload, diagnostics) as T;
135
+ const secretValues = normalizeSecretValues(options.secretValues);
136
+ const scrubbed = scrubObject(payload, diagnostics, secretValues) as T;
86
137
 
87
138
  if (typeof scrubbed.exception === "object" && scrubbed.exception !== null) {
88
139
  const exception = scrubbed.exception as Record<string, unknown>;
@@ -0,0 +1,184 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import type { DiagnosticCode } from "../../compiler/diagnostics/codes.ts";
3
+ import {
4
+ FORGE_WEBHOOK_REPLAY_DETECTED,
5
+ FORGE_WEBHOOK_SIGNATURE_INVALID,
6
+ FORGE_WEBHOOK_TIMESTAMP_INVALID,
7
+ } from "../../compiler/diagnostics/codes.ts";
8
+
9
+ export type WebhookProvider = "generic" | "github" | "stripe";
10
+
11
+ export interface WebhookReplayStore {
12
+ has(eventId: string): boolean | Promise<boolean>;
13
+ add(eventId: string): void | Promise<void>;
14
+ }
15
+
16
+ export interface WebhookVerificationInput {
17
+ provider: WebhookProvider;
18
+ secret: string;
19
+ payload: string | Uint8Array;
20
+ signatureHeader: string | null | undefined;
21
+ timestampHeader?: string | null;
22
+ eventId?: string;
23
+ replayStore?: WebhookReplayStore;
24
+ nowSeconds?: number;
25
+ toleranceSeconds?: number;
26
+ }
27
+
28
+ export interface WebhookVerificationResult {
29
+ ok: boolean;
30
+ code?: DiagnosticCode;
31
+ reason?: string;
32
+ provider: WebhookProvider;
33
+ }
34
+
35
+ export class MemoryWebhookReplayStore implements WebhookReplayStore {
36
+ private readonly seen = new Set<string>();
37
+
38
+ has(eventId: string): boolean {
39
+ return this.seen.has(eventId);
40
+ }
41
+
42
+ add(eventId: string): void {
43
+ this.seen.add(eventId);
44
+ }
45
+ }
46
+
47
+ function bytes(input: string | Uint8Array): Uint8Array {
48
+ return typeof input === "string" ? Buffer.from(input, "utf8") : input;
49
+ }
50
+
51
+ function hmacHex(secret: string, payload: string | Uint8Array): string {
52
+ return createHmac("sha256", secret).update(bytes(payload)).digest("hex");
53
+ }
54
+
55
+ function safeEqualHex(left: string | undefined, right: string): boolean {
56
+ if (!left || !/^[0-9a-f]+$/i.test(left) || !/^[0-9a-f]+$/i.test(right)) {
57
+ return false;
58
+ }
59
+ const leftBuffer = Buffer.from(left, "hex");
60
+ const rightBuffer = Buffer.from(right, "hex");
61
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
62
+ }
63
+
64
+ function fail(
65
+ provider: WebhookProvider,
66
+ code: DiagnosticCode,
67
+ reason: string,
68
+ ): WebhookVerificationResult {
69
+ return { ok: false, provider, code, reason };
70
+ }
71
+
72
+ function parseStripeHeader(header: string): { timestamp?: number; signatures: string[] } {
73
+ const signatures: string[] = [];
74
+ let timestamp: number | undefined;
75
+ for (const part of header.split(",")) {
76
+ const [key, value] = part.split("=", 2).map((item) => item.trim());
77
+ if (key === "t") {
78
+ const parsed = Number(value);
79
+ if (Number.isFinite(parsed)) {
80
+ timestamp = parsed;
81
+ }
82
+ }
83
+ if (key === "v1" && value) {
84
+ signatures.push(value);
85
+ }
86
+ }
87
+ return { timestamp, signatures };
88
+ }
89
+
90
+ function validateTimestamp(input: {
91
+ provider: WebhookProvider;
92
+ timestamp?: number;
93
+ nowSeconds: number;
94
+ toleranceSeconds: number;
95
+ }): WebhookVerificationResult | null {
96
+ if (!Number.isFinite(input.timestamp)) {
97
+ return fail(input.provider, FORGE_WEBHOOK_TIMESTAMP_INVALID, "missing or invalid webhook timestamp");
98
+ }
99
+ if (Math.abs(input.nowSeconds - input.timestamp!) > input.toleranceSeconds) {
100
+ return fail(input.provider, FORGE_WEBHOOK_TIMESTAMP_INVALID, "webhook timestamp is outside the replay window");
101
+ }
102
+ return null;
103
+ }
104
+
105
+ async function validateReplay(input: {
106
+ provider: WebhookProvider;
107
+ eventId?: string;
108
+ replayStore?: WebhookReplayStore;
109
+ }): Promise<WebhookVerificationResult | null> {
110
+ if (!input.eventId || !input.replayStore) {
111
+ return null;
112
+ }
113
+ if (await input.replayStore.has(input.eventId)) {
114
+ return fail(input.provider, FORGE_WEBHOOK_REPLAY_DETECTED, "webhook event id was already processed");
115
+ }
116
+ await input.replayStore.add(input.eventId);
117
+ return null;
118
+ }
119
+
120
+ export async function verifyWebhookSignature(
121
+ input: WebhookVerificationInput,
122
+ ): Promise<WebhookVerificationResult> {
123
+ const nowSeconds = input.nowSeconds ?? Math.floor(Date.now() / 1000);
124
+ const toleranceSeconds = input.toleranceSeconds ?? 300;
125
+ const signatureHeader = input.signatureHeader ?? "";
126
+
127
+ if (!input.secret || !signatureHeader) {
128
+ return fail(input.provider, FORGE_WEBHOOK_SIGNATURE_INVALID, "missing webhook secret or signature");
129
+ }
130
+
131
+ let valid = false;
132
+ let timestamp: number | undefined;
133
+
134
+ if (input.provider === "stripe") {
135
+ const parsed = parseStripeHeader(signatureHeader);
136
+ timestamp = parsed.timestamp;
137
+ const timestampFailure = validateTimestamp({
138
+ provider: input.provider,
139
+ timestamp,
140
+ nowSeconds,
141
+ toleranceSeconds,
142
+ });
143
+ if (timestampFailure) {
144
+ return timestampFailure;
145
+ }
146
+ const signedPayload = `${timestamp}.${Buffer.from(bytes(input.payload)).toString("utf8")}`;
147
+ const expected = hmacHex(input.secret, signedPayload);
148
+ valid = parsed.signatures.some((signature) => safeEqualHex(signature, expected));
149
+ } else if (input.provider === "github") {
150
+ const provided = signatureHeader.startsWith("sha256=")
151
+ ? signatureHeader.slice("sha256=".length)
152
+ : signatureHeader;
153
+ valid = safeEqualHex(provided, hmacHex(input.secret, input.payload));
154
+ } else {
155
+ const timestampRaw = input.timestampHeader ?? undefined;
156
+ timestamp = timestampRaw === undefined ? undefined : Number(timestampRaw);
157
+ if (timestampRaw !== undefined) {
158
+ const timestampFailure = validateTimestamp({
159
+ provider: input.provider,
160
+ timestamp,
161
+ nowSeconds,
162
+ toleranceSeconds,
163
+ });
164
+ if (timestampFailure) {
165
+ return timestampFailure;
166
+ }
167
+ }
168
+ const signedPayload = timestampRaw === undefined
169
+ ? input.payload
170
+ : `${timestampRaw}.${Buffer.from(bytes(input.payload)).toString("utf8")}`;
171
+ valid = safeEqualHex(signatureHeader, hmacHex(input.secret, signedPayload));
172
+ }
173
+
174
+ if (!valid) {
175
+ return fail(input.provider, FORGE_WEBHOOK_SIGNATURE_INVALID, "webhook signature did not match payload");
176
+ }
177
+
178
+ const replayFailure = await validateReplay(input);
179
+ if (replayFailure) {
180
+ return replayFailure;
181
+ }
182
+
183
+ return { ok: true, provider: input.provider };
184
+ }
@@ -11,11 +11,50 @@ export interface ForgeTelemetry {
11
11
  capture(name: string, payload?: Record<string, unknown>): Promise<void> | void;
12
12
  }
13
13
 
14
+ export type ForgeAiProvider = "openai" | "anthropic" | "gateway";
15
+
16
+ export interface ForgeAiUsage {
17
+ promptTokens: number;
18
+ completionTokens: number;
19
+ totalTokens: number;
20
+ }
21
+
22
+ export interface ForgeRunAgentInput {
23
+ provider?: ForgeAiProvider;
24
+ model: string;
25
+ prompt: string;
26
+ instructions: string;
27
+ purpose?: string;
28
+ tools?: Record<string, ForgeAiToolDefinition>;
29
+ stopWhen?: ForgeAgentStopWhen;
30
+ maxSteps?: number;
31
+ temperature?: number;
32
+ maxTokens?: number;
33
+ }
34
+
35
+ export interface ForgeRunAgentResult {
36
+ text: string;
37
+ provider: ForgeAiProvider;
38
+ model: string;
39
+ purpose?: string;
40
+ usage: ForgeAiUsage;
41
+ latencyMs: number;
42
+ toolCalls: Array<{ toolName: string; input: unknown }>;
43
+ toolResults: Array<{ toolName: string; output: unknown }>;
44
+ steps: number;
45
+ estimatedCostUsd?: number;
46
+ }
47
+
48
+ export interface ForgeAgentRuntime {
49
+ run(input: ForgeRunAgentInput): Promise<ForgeRunAgentResult>;
50
+ }
51
+
14
52
  export interface ForgeContext {
15
53
  db: ForgeRecord;
16
54
  emit(event: string, payload?: Record<string, unknown>): Promise<void> | void;
17
55
  telemetry: ForgeTelemetry;
18
56
  secrets: ForgeRecord;
57
+ agent?: ForgeAgentRuntime;
19
58
  auth?: {
20
59
  userId?: string;
21
60
  tenantId?: string;
@@ -44,6 +83,41 @@ export type ForgeActionDefinition<TEvent = unknown, TResult = unknown> = Record<
44
83
  handler: (ctx: ForgeContext, event: TEvent) => TResult | Promise<TResult>;
45
84
  };
46
85
 
86
+ export type ForgeAiToolRisk = "read" | "write" | "external" | "destructive";
87
+
88
+ export interface ForgeAiToolRuntimeContext {
89
+ secrets: ForgeRecord;
90
+ env: Record<string, string | undefined>;
91
+ telemetry: ForgeTelemetry;
92
+ auth?: ForgeContext["auth"];
93
+ }
94
+
95
+ export type ForgeAiToolDefinition<TArgs = unknown, TResult = unknown> = Record<string, unknown> & {
96
+ description: string;
97
+ inputSchema: unknown;
98
+ outputSchema?: unknown;
99
+ strict?: boolean;
100
+ needsApproval?: boolean | ((args: TArgs) => boolean | Promise<boolean>);
101
+ risk?: ForgeAiToolRisk;
102
+ handler: (
103
+ ctx: ForgeAiToolRuntimeContext,
104
+ args: TArgs,
105
+ ) => TResult | Promise<TResult>;
106
+ };
107
+
108
+ export type ForgeAgentStopWhen =
109
+ | { kind: "stepCount"; maxSteps: number }
110
+ | { kind: "toolCall"; toolName: string };
111
+
112
+ export type ForgeAgentDefinition = Record<string, unknown> & {
113
+ provider?: "openai" | "anthropic" | "gateway";
114
+ model: string;
115
+ instructions: string;
116
+ tools?: Record<string, ForgeAiToolDefinition> | string[];
117
+ stopWhen?: ForgeAgentStopWhen;
118
+ maxSteps?: number;
119
+ };
120
+
47
121
  export function defineTable<T extends Record<string, unknown>>(definition: T): T {
48
122
  return definition;
49
123
  }
@@ -80,6 +154,14 @@ export function action<T extends ForgeActionDefinition>(definition: T): ForgeDef
80
154
  return definition;
81
155
  }
82
156
 
157
+ export function aiTool<T extends ForgeAiToolDefinition>(definition: T): ForgeDefinition<T> {
158
+ return definition;
159
+ }
160
+
161
+ export function agent<T extends ForgeAgentDefinition>(definition: T): ForgeDefinition<T> {
162
+ return definition;
163
+ }
164
+
83
165
  export function event(name: string): { kind: "event"; name: string } {
84
166
  return { kind: "event", name };
85
167
  }
@@ -94,3 +176,14 @@ export function step<T extends (...args: any[]) => unknown>(
94
176
  export function workflow<T extends Record<string, unknown>>(definition: T): ForgeDefinition<T> {
95
177
  return definition;
96
178
  }
179
+
180
+ export {
181
+ MemoryWebhookReplayStore,
182
+ verifyWebhookSignature,
183
+ } from "./runtime/webhooks/security.ts";
184
+ export type {
185
+ WebhookProvider,
186
+ WebhookReplayStore,
187
+ WebhookVerificationInput,
188
+ WebhookVerificationResult,
189
+ } from "./runtime/webhooks/security.ts";
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.2";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.4";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;
@@ -24,6 +24,7 @@
24
24
  "zod": "^3.25.76"
25
25
  },
26
26
  "devDependencies": {
27
+ "@types/node": "^24.0.0",
27
28
  "typescript": "^5.7.3"
28
29
  },
29
30
  "forge": {
@@ -6,7 +6,10 @@
6
6
  "strict": true,
7
7
  "noEmit": true,
8
8
  "skipLibCheck": true,
9
- "allowImportingTsExtensions": true
9
+ "allowImportingTsExtensions": true,
10
+ "types": [
11
+ "node"
12
+ ]
10
13
  },
11
14
  "include": [
12
15
  "src/**/*.ts"