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.
- package/AGENTS.md +38 -3
- package/CHANGELOG.md +29 -0
- package/README.md +25 -10
- package/package.json +8 -5
- package/src/forge/_generated/actionSubscriptions.json +2 -2
- package/src/forge/_generated/actionSubscriptions.ts +3 -3
- package/src/forge/_generated/agentAdapterManifest.json +2 -2
- package/src/forge/_generated/agentAdapterManifest.ts +3 -3
- package/src/forge/_generated/agentContract.json +2 -2
- package/src/forge/_generated/agentContract.ts +183 -50
- package/src/forge/_generated/agentQuickstart.md +3 -1
- package/src/forge/_generated/agentTools.json +2 -0
- package/src/forge/_generated/agentTools.md +16 -0
- package/src/forge/_generated/agentTools.ts +12 -0
- package/src/forge/_generated/aiContext.ts +67 -1
- package/src/forge/_generated/aiModels.json +2 -2
- package/src/forge/_generated/aiModels.ts +17 -1
- package/src/forge/_generated/aiProviders.json +1 -1
- package/src/forge/_generated/aiProviders.ts +1 -1
- package/src/forge/_generated/aiRegistry.json +2 -2
- package/src/forge/_generated/aiRegistry.ts +7 -5
- package/src/forge/_generated/api.json +2 -2
- package/src/forge/_generated/api.ts +1 -1
- package/src/forge/_generated/appGraph.json +2 -2
- package/src/forge/_generated/appGraph.ts +512 -260
- package/src/forge/_generated/appMap.md +21 -1
- package/src/forge/_generated/artifactManifest.json +2 -2
- package/src/forge/_generated/artifactManifest.ts +2 -2
- package/src/forge/_generated/authClaims.json +1 -1
- package/src/forge/_generated/authClaims.ts +1 -1
- package/src/forge/_generated/authConfig.json +1 -1
- package/src/forge/_generated/authConfig.ts +1 -1
- package/src/forge/_generated/authContext.ts +1 -1
- package/src/forge/_generated/authRegistry.json +1 -1
- package/src/forge/_generated/authRegistry.ts +1 -1
- package/src/forge/_generated/buildInfo.json +2 -2
- package/src/forge/_generated/buildInfo.ts +4 -4
- package/src/forge/_generated/capabilityMap.json +2 -2
- package/src/forge/_generated/capabilityMap.md +1 -1
- package/src/forge/_generated/capabilityMap.ts +2 -2
- package/src/forge/_generated/client.ts +1 -1
- package/src/forge/_generated/clientApi.ts +1 -1
- package/src/forge/_generated/clientManifest.json +2 -2
- package/src/forge/_generated/clientManifest.ts +3 -3
- package/src/forge/_generated/clientTypes.ts +1 -1
- package/src/forge/_generated/configRegistry.json +1 -1
- package/src/forge/_generated/configRegistry.ts +1 -1
- package/src/forge/_generated/dataGraph.json +2 -2
- package/src/forge/_generated/dataGraph.ts +3 -3
- package/src/forge/_generated/db.json +1 -1
- package/src/forge/_generated/db.ts +1 -1
- package/src/forge/_generated/dbSecurityManifest.json +1 -1
- package/src/forge/_generated/dbSecurityManifest.ts +1 -1
- package/src/forge/_generated/dbSessionContext.json +1 -1
- package/src/forge/_generated/dbSessionContext.ts +1 -1
- package/src/forge/_generated/deployManifest.json +2 -2
- package/src/forge/_generated/deployManifest.ts +7 -7
- package/src/forge/_generated/devManifest.json +2 -2
- package/src/forge/_generated/devManifest.ts +18 -3
- package/src/forge/_generated/envSchema.json +1 -1
- package/src/forge/_generated/envSchema.ts +1 -1
- package/src/forge/_generated/frontendGraph.json +1 -1
- package/src/forge/_generated/frontendGraph.ts +1 -1
- package/src/forge/_generated/importGuards.json +1 -1
- package/src/forge/_generated/importGuards.ts +1 -1
- package/src/forge/_generated/index.ts +2 -1
- package/src/forge/_generated/liveProductionManifest.json +1 -1
- package/src/forge/_generated/liveProductionManifest.ts +1 -1
- package/src/forge/_generated/liveProtocol.json +1 -1
- package/src/forge/_generated/liveProtocol.ts +1 -1
- package/src/forge/_generated/liveQueryRegistry.json +2 -2
- package/src/forge/_generated/liveQueryRegistry.ts +3 -3
- package/src/forge/_generated/liveTransportConfig.json +1 -1
- package/src/forge/_generated/liveTransportConfig.ts +1 -1
- package/src/forge/_generated/makeRegistry.json +2 -2
- package/src/forge/_generated/makeRegistry.ts +16 -2
- package/src/forge/_generated/makeTemplates.json +2 -2
- package/src/forge/_generated/makeTemplates.ts +6 -1
- package/src/forge/_generated/mockMap.json +1 -1
- package/src/forge/_generated/mockMap.ts +1 -1
- package/src/forge/_generated/operationPlaybooks.md +34 -14
- package/src/forge/_generated/packageGraph.json +2 -2
- package/src/forge/_generated/packageGraph.ts +8808 -4723
- package/src/forge/_generated/packageUpgradeRegistry.json +2 -2
- package/src/forge/_generated/packageUpgradeRegistry.ts +2 -2
- package/src/forge/_generated/permissionMatrix.json +2 -2
- package/src/forge/_generated/permissionMatrix.ts +3 -3
- package/src/forge/_generated/policyRegistry.json +2 -2
- package/src/forge/_generated/policyRegistry.ts +3 -3
- package/src/forge/_generated/queryRegistry.json +2 -2
- package/src/forge/_generated/queryRegistry.ts +3 -3
- package/src/forge/_generated/react.d.ts +1 -1
- package/src/forge/_generated/react.ts +1 -1
- package/src/forge/_generated/reactManifest.json +2 -2
- package/src/forge/_generated/reactManifest.ts +3 -3
- package/src/forge/_generated/releaseManifest.json +2 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/_generated/rlsPolicies.json +1 -1
- package/src/forge/_generated/rlsPolicies.sql +1 -1
- package/src/forge/_generated/rlsPolicies.ts +1 -1
- package/src/forge/_generated/runtimeGraph.json +2 -2
- package/src/forge/_generated/runtimeGraph.ts +3 -3
- package/src/forge/_generated/runtimeMatrix.json +2 -2
- package/src/forge/_generated/runtimeMatrix.ts +8684 -1939
- package/src/forge/_generated/runtimeRegistry.ts +1 -1
- package/src/forge/_generated/runtimeRules.md +13 -1
- package/src/forge/_generated/secretRegistry.json +1 -1
- package/src/forge/_generated/secretRegistry.ts +1 -1
- package/src/forge/_generated/secretsContext.ts +1 -1
- package/src/forge/_generated/serverApi.ts +1 -1
- package/src/forge/_generated/sourceMapManifest.json +2 -2
- package/src/forge/_generated/sourceMapManifest.ts +2 -2
- package/src/forge/_generated/sqlPlan.json +1 -1
- package/src/forge/_generated/sqlPlan.ts +1 -1
- package/src/forge/_generated/subscriptionManifest.json +2 -2
- package/src/forge/_generated/subscriptionManifest.ts +3 -3
- package/src/forge/_generated/symbolicationManifest.json +2 -2
- package/src/forge/_generated/symbolicationManifest.ts +2 -2
- package/src/forge/_generated/telemetryRegistry.json +2 -2
- package/src/forge/_generated/telemetryRegistry.ts +3 -3
- package/src/forge/_generated/telemetrySinks.json +2 -2
- package/src/forge/_generated/telemetrySinks.ts +2 -2
- package/src/forge/_generated/tenantScope.json +2 -2
- package/src/forge/_generated/tenantScope.ts +3 -3
- package/src/forge/_generated/testGraph.json +2 -2
- package/src/forge/_generated/testGraph.ts +339 -17
- package/src/forge/_generated/testPlanRegistry.json +2 -2
- package/src/forge/_generated/testPlanRegistry.ts +2 -2
- package/src/forge/_generated/uiRoutes.json +1 -1
- package/src/forge/_generated/uiRoutes.ts +1 -1
- package/src/forge/_generated/uiScenarios.json +1 -1
- package/src/forge/_generated/uiScenarios.ts +1 -1
- package/src/forge/_generated/uiTestManifest.json +2 -2
- package/src/forge/_generated/uiTestManifest.ts +2 -2
- package/src/forge/_generated/workflowRegistry.json +2 -2
- package/src/forge/_generated/workflowRegistry.ts +3 -3
- package/src/forge/_generated/workflowSubscriptions.json +2 -2
- package/src/forge/_generated/workflowSubscriptions.ts +3 -3
- package/src/forge/cli/ai.ts +351 -1
- package/src/forge/cli/auth.ts +36 -1
- package/src/forge/cli/commands.ts +19 -0
- package/src/forge/cli/parse.ts +67 -8
- package/src/forge/cli/rls.ts +529 -17
- package/src/forge/cli/secrets.ts +46 -1
- package/src/forge/cli/security.ts +269 -0
- package/src/forge/compiler/agent-contract/build.ts +289 -8
- package/src/forge/compiler/agent-contract/types.ts +43 -0
- package/src/forge/compiler/ai-registry/build.ts +62 -1
- package/src/forge/compiler/ai-registry/constants.ts +1 -1
- package/src/forge/compiler/ai-registry/parse.ts +98 -4
- package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
- package/src/forge/compiler/dev-manifest/build.ts +3 -0
- package/src/forge/compiler/diagnostics/codes.ts +15 -0
- package/src/forge/compiler/diagnostics/create.ts +1 -1
- package/src/forge/compiler/make-registry/build.ts +13 -0
- package/src/forge/compiler/orchestrator/plan.ts +11 -0
- package/src/forge/compiler/orchestrator/serialize.ts +68 -0
- package/src/forge/compiler/package-graph/compiler.ts +13 -3
- package/src/forge/compiler/types/ai-registry.ts +25 -1
- package/src/forge/compiler/types/app-graph.ts +1 -0
- package/src/forge/compiler/types/cli.ts +1 -0
- package/src/forge/compiler/types/dev-manifest.ts +3 -0
- package/src/forge/dev/server.ts +508 -1
- package/src/forge/make/index.ts +126 -3
- package/src/forge/make/templates.ts +188 -0
- package/src/forge/make/types.ts +1 -0
- package/src/forge/runtime/ai/context.ts +210 -5
- package/src/forge/runtime/ai/types.ts +70 -0
- package/src/forge/runtime/auth/claims.ts +32 -0
- package/src/forge/runtime/auth/errors.ts +2 -0
- package/src/forge/runtime/context/create-context.ts +30 -6
- package/src/forge/runtime/db/memory-adapter.ts +2 -2
- package/src/forge/runtime/telemetry/scrubber.ts +56 -5
- package/src/forge/runtime/webhooks/security.ts +184 -0
- package/src/forge/server.ts +93 -0
- package/src/forge/version.ts +1 -1
- package/templates/b2b-support-web/package.json +1 -0
- package/templates/b2b-support-web/tsconfig.json +4 -1
- package/templates/minimal-web/package.json +1 -0
- 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?: {
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/forge/server.ts
CHANGED
|
@@ -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";
|
package/src/forge/version.ts
CHANGED