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
@@ -1,4 +1,12 @@
1
+ import { join } from "node:path";
1
2
  import { createDiagnostic } from "../compiler/diagnostics/create.ts";
3
+ import type { SqlPlan } from "../compiler/data-graph/sql/types.ts";
4
+ import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
5
+ import { nodeFileSystem } from "../compiler/fs/index.ts";
6
+ import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
7
+ import { createDbAdapter } from "../runtime/db/factory.ts";
8
+ import type { DbAdapterKind } from "../runtime/db/adapter.ts";
9
+ import { applyMigrations } from "../runtime/db/migrate.ts";
2
10
  import { createAiContext } from "../runtime/ai/context.ts";
3
11
  import {
4
12
  checkAiProviders,
@@ -10,11 +18,36 @@ import { enqueueMockAiResponse } from "../runtime/ai/mock.ts";
10
18
  import { getRuntimeEnvStore, initializeRuntimeEnv } from "../runtime/context/create-context.ts";
11
19
  import { createNoopTelemetryContext } from "../runtime/telemetry/context.ts";
12
20
  import { generateTraceId } from "../runtime/telemetry/correlation.ts";
21
+ import { inspectTrace } from "../runtime/telemetry/flush.ts";
13
22
  import { loadSecretRegistry } from "../runtime/secrets/check.ts";
14
23
  import { createRuntimeSecretsBundle } from "../runtime/secrets/runtime-bundle.ts";
15
24
  import type { ForgeAiProvider } from "../runtime/ai/types.ts";
16
25
 
17
- export type AiSubcommand = "providers" | "check" | "test" | "models";
26
+ function loadAgentTools(workspaceRoot: string): { explicitTools?: unknown[]; autoTools?: unknown[]; agents?: unknown[] } | null {
27
+ const path = join(workspaceRoot, GENERATED_DIR, "agentTools.json");
28
+ if (!nodeFileSystem.exists(path)) {
29
+ return null;
30
+ }
31
+ try {
32
+ return JSON.parse(stripDeterministicHeader(nodeFileSystem.readText(path) ?? "{}")) as {
33
+ explicitTools?: unknown[];
34
+ autoTools?: unknown[];
35
+ agents?: unknown[];
36
+ };
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export type AiSubcommand =
43
+ | "providers"
44
+ | "check"
45
+ | "test"
46
+ | "models"
47
+ | "tools"
48
+ | "agents"
49
+ | "redteam"
50
+ | "trace";
18
51
 
19
52
  export interface AiCommandOptions {
20
53
  subcommand: AiSubcommand;
@@ -24,6 +57,9 @@ export interface AiCommandOptions {
24
57
  model?: string;
25
58
  prompt?: string;
26
59
  mock?: boolean;
60
+ traceId?: string;
61
+ db?: DbAdapterKind;
62
+ databaseUrl?: string;
27
63
  }
28
64
 
29
65
  export interface AiCommandResult {
@@ -32,6 +68,246 @@ export interface AiCommandResult {
32
68
  diagnostics?: ReturnType<typeof createDiagnostic>[];
33
69
  }
34
70
 
71
+ export interface AiTraceSummary {
72
+ traceId: string;
73
+ agents: Array<Record<string, unknown>>;
74
+ tools: Array<Record<string, unknown>>;
75
+ generations: Array<Record<string, unknown>>;
76
+ usage: Array<Record<string, unknown>>;
77
+ failures: Array<Record<string, unknown>>;
78
+ events: Array<Record<string, unknown>>;
79
+ spans: Array<Record<string, unknown>>;
80
+ }
81
+
82
+ function readGeneratedJson<T>(workspaceRoot: string, relative: string): T | null {
83
+ const path = join(workspaceRoot, relative);
84
+ if (!nodeFileSystem.exists(path)) {
85
+ return null;
86
+ }
87
+ try {
88
+ return JSON.parse(stripDeterministicHeader(nodeFileSystem.readText(path) ?? "{}")) as T;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function payloadOf(event: Record<string, unknown>): Record<string, unknown> {
95
+ const payload = event.payload;
96
+ if (typeof payload === "string") {
97
+ try {
98
+ return JSON.parse(payload) as Record<string, unknown>;
99
+ } catch {
100
+ return {};
101
+ }
102
+ }
103
+ return payload && typeof payload === "object" ? payload as Record<string, unknown> : {};
104
+ }
105
+
106
+ function eventName(payload: Record<string, unknown>): string {
107
+ const event = payload.event;
108
+ if (event && typeof event === "object" && "name" in event) {
109
+ return String((event as { name?: unknown }).name ?? "");
110
+ }
111
+ return "";
112
+ }
113
+
114
+ function eventProperties(payload: Record<string, unknown>): Record<string, unknown> {
115
+ const event = payload.event;
116
+ if (event && typeof event === "object" && "properties" in event) {
117
+ const properties = (event as { properties?: unknown }).properties;
118
+ return properties && typeof properties === "object" ? properties as Record<string, unknown> : {};
119
+ }
120
+ return {};
121
+ }
122
+
123
+ interface AgentToolRecord {
124
+ name?: unknown;
125
+ risk?: unknown;
126
+ needsApproval?: unknown;
127
+ readOnly?: unknown;
128
+ sourceKind?: unknown;
129
+ }
130
+
131
+ interface AgentRecord {
132
+ name?: unknown;
133
+ stopWhen?: unknown;
134
+ maxSteps?: unknown;
135
+ }
136
+
137
+ interface AiRedteamScenario {
138
+ id: string;
139
+ name: string;
140
+ status: "passed" | "failed" | "not-applicable";
141
+ evidence: string;
142
+ }
143
+
144
+ function toolName(tool: AgentToolRecord): string {
145
+ return typeof tool.name === "string" && tool.name.length > 0 ? tool.name : "<anonymous>";
146
+ }
147
+
148
+ function toolNeedsApproval(tool: AgentToolRecord): boolean {
149
+ return tool.needsApproval === true || tool.needsApproval === "dynamic";
150
+ }
151
+
152
+ function isDangerousRisk(risk: unknown): boolean {
153
+ return risk === "write" || risk === "external" || risk === "destructive";
154
+ }
155
+
156
+ function hasAgentStepLimit(agent: AgentRecord): boolean {
157
+ if (typeof agent.maxSteps === "number" && agent.maxSteps > 0) {
158
+ return true;
159
+ }
160
+ const stopWhen = agent.stopWhen;
161
+ return Boolean(
162
+ stopWhen &&
163
+ typeof stopWhen === "object" &&
164
+ "maxSteps" in stopWhen &&
165
+ typeof (stopWhen as { maxSteps?: unknown }).maxSteps === "number" &&
166
+ (stopWhen as { maxSteps: number }).maxSteps > 0,
167
+ );
168
+ }
169
+
170
+ function runAgentRedteam(workspaceRoot: string): AiCommandResult {
171
+ const agentTools = loadAgentTools(workspaceRoot);
172
+ const explicitTools = (agentTools?.explicitTools ?? []) as AgentToolRecord[];
173
+ const autoTools = (agentTools?.autoTools ?? []) as AgentToolRecord[];
174
+ const agents = (agentTools?.agents ?? []) as AgentRecord[];
175
+ const allTools = [...explicitTools, ...autoTools];
176
+ const dangerousTools = allTools.filter((tool) => isDangerousRisk(tool.risk));
177
+ const dangerousWithoutApproval = dangerousTools.filter((tool) => !toolNeedsApproval(tool));
178
+ const readToolsWithWriteSurface = allTools.filter(
179
+ (tool) => tool.risk === "read" && tool.readOnly === false,
180
+ );
181
+ const commandToolsWithoutApproval = autoTools.filter(
182
+ (tool) => tool.sourceKind === "command" && !toolNeedsApproval(tool),
183
+ );
184
+ const agentsWithoutStepLimit = agents.filter((agent) => !hasAgentStepLimit(agent));
185
+ const secretLikeTools = allTools.filter((tool) => /secret|token|api[_-]?key/i.test(toolName(tool)));
186
+
187
+ const scenarios: AiRedteamScenario[] = [
188
+ {
189
+ id: "approval-bypass",
190
+ name: "Dangerous tools require approval",
191
+ status:
192
+ dangerousTools.length === 0
193
+ ? "not-applicable"
194
+ : dangerousWithoutApproval.length === 0
195
+ ? "passed"
196
+ : "failed",
197
+ evidence:
198
+ dangerousWithoutApproval.length === 0
199
+ ? `${dangerousTools.length} dangerous tools require approval`
200
+ : `tools without approval: ${dangerousWithoutApproval.map(toolName).sort().join(", ")}`,
201
+ },
202
+ {
203
+ id: "auto-command-approval",
204
+ name: "Generated command auto-tools require approval",
205
+ status:
206
+ autoTools.filter((tool) => tool.sourceKind === "command").length === 0
207
+ ? "not-applicable"
208
+ : commandToolsWithoutApproval.length === 0
209
+ ? "passed"
210
+ : "failed",
211
+ evidence:
212
+ commandToolsWithoutApproval.length === 0
213
+ ? "command auto-tools are approval gated"
214
+ : `command auto-tools without approval: ${commandToolsWithoutApproval.map(toolName).sort().join(", ")}`,
215
+ },
216
+ {
217
+ id: "read-only-boundary",
218
+ name: "Read tools stay read-only",
219
+ status: readToolsWithWriteSurface.length === 0 ? "passed" : "failed",
220
+ evidence:
221
+ readToolsWithWriteSurface.length === 0
222
+ ? "read tools do not expose write metadata"
223
+ : `read tools with write surface: ${readToolsWithWriteSurface.map(toolName).sort().join(", ")}`,
224
+ },
225
+ {
226
+ id: "excessive-agency",
227
+ name: "Agents have bounded step limits",
228
+ status:
229
+ agents.length === 0
230
+ ? "not-applicable"
231
+ : agentsWithoutStepLimit.length === 0
232
+ ? "passed"
233
+ : "failed",
234
+ evidence:
235
+ agentsWithoutStepLimit.length === 0
236
+ ? `${agents.length} agents are step bounded`
237
+ : `agents without step limits: ${agentsWithoutStepLimit.map((agent) => String(agent.name ?? "<anonymous>")).sort().join(", ")}`,
238
+ },
239
+ {
240
+ id: "secret-extraction-surface",
241
+ name: "Tool names do not expose secret-like capabilities",
242
+ status: secretLikeTools.length === 0 ? "passed" : "failed",
243
+ evidence:
244
+ secretLikeTools.length === 0
245
+ ? "no secret-like tool names detected"
246
+ : `secret-like tools: ${secretLikeTools.map(toolName).sort().join(", ")}`,
247
+ },
248
+ ];
249
+
250
+ const failed = scenarios.filter((scenario) => scenario.status === "failed");
251
+ return {
252
+ exitCode: failed.length === 0 ? 0 : 1,
253
+ data: {
254
+ schemaVersion: "0.1.0",
255
+ kind: "agent-redteam",
256
+ ok: failed.length === 0,
257
+ assurance: "structural-redteam",
258
+ scenarios,
259
+ summary: {
260
+ passed: scenarios.filter((scenario) => scenario.status === "passed").map((scenario) => scenario.id),
261
+ failed: failed.map((scenario) => scenario.id),
262
+ notApplicable: scenarios
263
+ .filter((scenario) => scenario.status === "not-applicable")
264
+ .map((scenario) => scenario.id),
265
+ },
266
+ },
267
+ diagnostics: failed.map((scenario) =>
268
+ createDiagnostic({
269
+ severity: "error",
270
+ code: "FORGE_AI_REDTEAM_FAILED",
271
+ message: `${scenario.name}: ${scenario.evidence}`,
272
+ fixHint: "Review generated agentTools.json and require approval for write/external/destructive tools, read-only metadata for read tools, and max step limits for agents.",
273
+ suggestedCommands: ["forge ai tools --json", "forge ai agents --json", "forge ai redteam --json"],
274
+ docs: ["docs/ai-agents.md", "docs/threat-model.md"],
275
+ }),
276
+ ),
277
+ };
278
+ }
279
+
280
+ export function summarizeAiTrace(traceId: string, inspected: {
281
+ events: Record<string, unknown>[];
282
+ spans: Record<string, unknown>[];
283
+ }): AiTraceSummary {
284
+ const aiEvents = inspected.events
285
+ .map((event) => {
286
+ const payload = payloadOf(event);
287
+ const name = eventName(payload);
288
+ return {
289
+ id: event.id,
290
+ name,
291
+ status: event.status,
292
+ createdAt: event.created_at,
293
+ properties: eventProperties(payload),
294
+ };
295
+ })
296
+ .filter((event) => event.name.startsWith("forge.ai."));
297
+
298
+ const bySuffix = (suffix: string) => aiEvents.filter((event) => event.name.endsWith(suffix));
299
+ return {
300
+ traceId,
301
+ agents: aiEvents.filter((event) => event.name.startsWith("forge.ai.agent.")),
302
+ tools: aiEvents.filter((event) => event.name.startsWith("forge.ai.tool.")),
303
+ generations: aiEvents.filter((event) => event.name.startsWith("forge.ai.generation.") || event.name.startsWith("forge.ai.stream.")),
304
+ usage: aiEvents.filter((event) => event.name === "forge.ai.usage"),
305
+ failures: [...bySuffix(".failed")],
306
+ events: aiEvents,
307
+ spans: inspected.spans,
308
+ };
309
+ }
310
+
35
311
  export async function runAiCommand(options: AiCommandOptions): Promise<AiCommandResult> {
36
312
  initializeRuntimeEnv(options.workspaceRoot);
37
313
  const registry = loadAiRegistry(options.workspaceRoot);
@@ -47,6 +323,58 @@ export async function runAiCommand(options: AiCommandOptions): Promise<AiCommand
47
323
  const models = loadAiModels(options.workspaceRoot);
48
324
  return { exitCode: 0, data: { models } };
49
325
  }
326
+ case "tools": {
327
+ const agentTools = loadAgentTools(options.workspaceRoot);
328
+ return {
329
+ exitCode: 0,
330
+ data: {
331
+ explicitTools: agentTools?.explicitTools ?? registry?.tools ?? [],
332
+ autoTools: agentTools?.autoTools ?? [],
333
+ },
334
+ };
335
+ }
336
+ case "agents": {
337
+ const agentTools = loadAgentTools(options.workspaceRoot);
338
+ return { exitCode: 0, data: { agents: agentTools?.agents ?? registry?.agents ?? [] } };
339
+ }
340
+ case "redteam": {
341
+ return runAgentRedteam(options.workspaceRoot);
342
+ }
343
+ case "trace": {
344
+ if (!options.traceId) {
345
+ return {
346
+ exitCode: 1,
347
+ diagnostics: [
348
+ createDiagnostic({
349
+ severity: "error",
350
+ code: "FORGE_CLI_USAGE",
351
+ message: "forge ai trace requires a trace id",
352
+ }),
353
+ ],
354
+ };
355
+ }
356
+ const { adapter, diagnostics } = await createDbAdapter({
357
+ kind: options.db ?? "pglite",
358
+ workspaceRoot: options.workspaceRoot,
359
+ databaseUrl: options.databaseUrl,
360
+ });
361
+ if (!adapter) {
362
+ return { exitCode: 1, diagnostics };
363
+ }
364
+ try {
365
+ const sqlPlan = readGeneratedJson<SqlPlan>(
366
+ options.workspaceRoot,
367
+ `${GENERATED_DIR}/sqlPlan.json`,
368
+ );
369
+ if (sqlPlan) {
370
+ await applyMigrations(adapter, sqlPlan);
371
+ }
372
+ const inspected = await inspectTrace(adapter, options.traceId);
373
+ return { exitCode: 0, data: summarizeAiTrace(options.traceId, inspected) };
374
+ } finally {
375
+ await adapter.close();
376
+ }
377
+ }
50
378
  case "check": {
51
379
  const result = checkAiProviders(store, registry, secretRegistry);
52
380
  return { exitCode: result.ok ? 0 : 1, data: result };
@@ -133,6 +461,28 @@ export function formatAiHuman(subcommand: AiSubcommand, result: AiCommandResult)
133
461
  return `${models.map((m) => JSON.stringify(m)).join("\n")}\n`;
134
462
  }
135
463
 
464
+ if (subcommand === "tools") {
465
+ const data = result.data as { explicitTools?: unknown[]; autoTools?: unknown[] };
466
+ const tools = [
467
+ ...(data.explicitTools ?? []),
468
+ ...(data.autoTools ?? []),
469
+ ];
470
+ return `${tools.map((m) => JSON.stringify(m)).join("\n")}\n`;
471
+ }
472
+
473
+ if (subcommand === "agents") {
474
+ const agents = (result.data as { agents: unknown[] })?.agents ?? [];
475
+ return `${agents.map((m) => JSON.stringify(m)).join("\n")}\n`;
476
+ }
477
+
478
+ if (subcommand === "trace") {
479
+ return `${JSON.stringify(result.data, null, 2)}\n`;
480
+ }
481
+
482
+ if (subcommand === "redteam") {
483
+ return `${JSON.stringify(result.data, null, 2)}\n`;
484
+ }
485
+
136
486
  if (subcommand === "check") {
137
487
  const data = result.data as { ok: boolean; missing: string[] };
138
488
  return data.ok
@@ -9,7 +9,7 @@ import { mapClaimsToAuthContext } from "../runtime/auth/claims.ts";
9
9
  import { ForgeAuthError } from "../runtime/auth/errors.ts";
10
10
  import { verifyJwtToken } from "../runtime/auth/verifier.ts";
11
11
 
12
- export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks";
12
+ export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove";
13
13
 
14
14
  export interface AuthCommandOptions {
15
15
  subcommand: AuthSubcommand;
@@ -163,6 +163,41 @@ export async function runAuthCommand(
163
163
  if (options.subcommand === "check") {
164
164
  return validateConfig(options.workspaceRoot);
165
165
  }
166
+ if (options.subcommand === "prove") {
167
+ const checked = validateConfig(options.workspaceRoot);
168
+ const config = loadAuthConfigFromEnv(options.workspaceRoot);
169
+ const productionMode = config.mode === "jwt" || config.mode === "oidc";
170
+ return {
171
+ ok: checked.ok,
172
+ mode: config.mode,
173
+ data: {
174
+ schemaVersion: "0.1.0",
175
+ kind: "auth-proof",
176
+ ok: checked.ok,
177
+ mode: config.mode,
178
+ productionReady: productionMode && checked.ok,
179
+ invariants: [
180
+ {
181
+ id: "INV-001",
182
+ name: "dev headers are not production auth",
183
+ status: productionMode ? "passed" : "local-only",
184
+ evidence: productionMode
185
+ ? "jwt/oidc mode configured through environment"
186
+ : "dev-headers mode is allowed only for local dev, tests, and agent workflows",
187
+ },
188
+ {
189
+ id: "INV-001-CONFIG",
190
+ name: "jwt/oidc required settings are present when production auth is enabled",
191
+ status: checked.ok ? "passed" : "failed",
192
+ evidence: checked.data,
193
+ },
194
+ ],
195
+ checkedAt: "deterministic",
196
+ },
197
+ error: checked.error,
198
+ exitCode: checked.exitCode,
199
+ };
200
+ }
166
201
  if (options.subcommand === "config") {
167
202
  return publicConfig(options.workspaceRoot);
168
203
  }
@@ -102,6 +102,11 @@ import {
102
102
  } from "./windows.ts";
103
103
  import { formatAuthHuman, formatAuthJson, runAuthCommand } from "./auth.ts";
104
104
  import { formatRlsHuman, formatRlsJson, runRlsCommand } from "./rls.ts";
105
+ import {
106
+ formatSecurityHuman,
107
+ formatSecurityJson,
108
+ runSecurityCommand,
109
+ } from "./security.ts";
105
110
  import { formatDepsHuman, formatDepsJson, runDepsCommand } from "./deps.ts";
106
111
  import {
107
112
  formatReleaseHuman,
@@ -405,6 +410,7 @@ export async function runInspectCommand(
405
410
  "test-graph": `${GENERATED_DIR}/testGraph.json`,
406
411
  "test-plans": `${GENERATED_DIR}/testPlanRegistry.json`,
407
412
  "agent-contract": `${GENERATED_DIR}/agentContract.json`,
413
+ "agent-tools": `${GENERATED_DIR}/agentTools.json`,
408
414
  "agent-adapters": `${GENERATED_DIR}/agentAdapterManifest.json`,
409
415
  "capability-map": `${GENERATED_DIR}/capabilityMap.json`,
410
416
  ui: `${GENERATED_DIR}/uiTestManifest.json`,
@@ -451,6 +457,7 @@ export async function runInspectCommand(
451
457
  ["testGraph", `${GENERATED_DIR}/testGraph.json`],
452
458
  ["testPlanRegistry", `${GENERATED_DIR}/testPlanRegistry.json`],
453
459
  ["agentContract", `${GENERATED_DIR}/agentContract.json`],
460
+ ["agentTools", `${GENERATED_DIR}/agentTools.json`],
454
461
  ["agentAdapters", `${GENERATED_DIR}/agentAdapterManifest.json`],
455
462
  ["capabilityMap", `${GENERATED_DIR}/capabilityMap.json`],
456
463
  ["ui", `${GENERATED_DIR}/uiTestManifest.json`],
@@ -634,6 +641,15 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
634
641
  }
635
642
  return result.exitCode;
636
643
  }
644
+ case "security": {
645
+ const result = await runSecurityCommand(command);
646
+ if (command.json) {
647
+ process.stdout.write(formatSecurityJson(result));
648
+ } else {
649
+ process.stdout.write(formatSecurityHuman(result));
650
+ }
651
+ return result.exitCode;
652
+ }
637
653
  case "auth": {
638
654
  const result = await runAuthCommand(command);
639
655
  if (command.json) {
@@ -1111,6 +1127,9 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1111
1127
  model: command.model,
1112
1128
  prompt: command.prompt,
1113
1129
  mock: command.mock,
1130
+ traceId: command.traceId,
1131
+ db: command.db,
1132
+ databaseUrl: command.databaseUrl,
1114
1133
  });
1115
1134
 
1116
1135
  if (command.json) {
@@ -17,6 +17,7 @@ import type { SelfHostSubcommand } from "./self-host.ts";
17
17
  import type { AgentContractSubcommand } from "./agent-contract.ts";
18
18
  import type { AuthSubcommand } from "./auth.ts";
19
19
  import type { RlsSubcommand } from "./rls.ts";
20
+ import type { SecuritySubcommand } from "./security.ts";
20
21
  import type { DepsSubcommand } from "./deps.ts";
21
22
  import type { ReleaseAction, ReleaseArea } from "./release.ts";
22
23
  import type { MakeCommandOptions, MakePrimitive } from "../make/types.ts";
@@ -99,6 +100,14 @@ export type ForgeCommand =
99
100
  }
100
101
  | { kind: "doctor"; target?: "project" | "windows"; json: boolean; workspaceRoot: string }
101
102
  | { kind: "setup"; target: "windows"; json: boolean; yes: boolean; workspaceRoot: string }
103
+ | {
104
+ kind: "security";
105
+ subcommand: SecuritySubcommand;
106
+ db: DbAdapterKind;
107
+ databaseUrl?: string;
108
+ json: boolean;
109
+ workspaceRoot: string;
110
+ }
102
111
  | {
103
112
  kind: "auth";
104
113
  subcommand: AuthSubcommand;
@@ -284,6 +293,9 @@ export type ForgeCommand =
284
293
  model?: string;
285
294
  prompt?: string;
286
295
  mock: boolean;
296
+ traceId?: string;
297
+ db?: DbAdapterKind;
298
+ databaseUrl?: string;
287
299
  workspaceRoot: string;
288
300
  };
289
301
 
@@ -305,6 +317,7 @@ export const TOP_LEVEL_COMMANDS = [
305
317
  "ui",
306
318
  "doctor",
307
319
  "setup",
320
+ "security",
308
321
  "auth",
309
322
  "rls",
310
323
  "deps",
@@ -367,6 +380,7 @@ export const INSPECT_TARGETS: InspectTarget[] = [
367
380
  "test-graph",
368
381
  "test-plans",
369
382
  "agent-contract",
383
+ "agent-tools",
370
384
  "agent-adapters",
371
385
  "capability-map",
372
386
  "framework",
@@ -392,8 +406,10 @@ const AUTH_SUBCOMMANDS: AuthSubcommand[] = [
392
406
  "decode",
393
407
  "test-token",
394
408
  "jwks",
409
+ "prove",
395
410
  ];
396
- const RLS_SUBCOMMANDS: RlsSubcommand[] = ["generate", "check", "apply", "test"];
411
+ const SECURITY_SUBCOMMANDS: SecuritySubcommand[] = ["prove"];
412
+ const RLS_SUBCOMMANDS: RlsSubcommand[] = ["generate", "check", "apply", "test", "mutate-test"];
397
413
  const DEPS_SUBCOMMANDS: DepsSubcommand[] = [
398
414
  "outdated",
399
415
  "inspect",
@@ -430,6 +446,7 @@ const MAKE_PRIMITIVES: MakePrimitive[] = [
430
446
  "component",
431
447
  "page",
432
448
  "ui",
449
+ "ai-chat",
433
450
  "resource",
434
451
  "apply",
435
452
  "rollback",
@@ -518,6 +535,16 @@ const UI_BROWSERS: UiBrowserName[] = ["chromium", "firefox", "webkit"];
518
535
  const UI_TRACE_MODES: UiTraceMode[] = ["on", "off", "retain-on-failure"];
519
536
  const UI_SCREENSHOT_MODES: UiScreenshotMode[] = ["on", "off", "only-on-failure"];
520
537
  const UI_VIDEO_MODES: UiVideoMode[] = ["on", "off", "retain-on-failure"];
538
+ const AI_SUBCOMMANDS: AiSubcommand[] = [
539
+ "providers",
540
+ "check",
541
+ "test",
542
+ "models",
543
+ "tools",
544
+ "agents",
545
+ "redteam",
546
+ "trace",
547
+ ];
521
548
 
522
549
  function parseFlag(args: string[], flag: string): boolean {
523
550
  return args.includes(flag);
@@ -549,6 +576,10 @@ function parseDbKind(value: string | undefined): "pglite" | "postgres" | "none"
549
576
  return "pglite";
550
577
  }
551
578
 
579
+ function parsePersistentDbKind(value: string | undefined): DbAdapterKind {
580
+ return parseDbKind(value) === "postgres" ? "postgres" : "pglite";
581
+ }
582
+
552
583
  function parseAdapterKind(value: string | undefined): DbAdapterKind {
553
584
  if (value === "postgres" || value === "memory") {
554
585
  return value;
@@ -969,10 +1000,29 @@ export function parseCli(argv: string[]): ParsedCli {
969
1000
  errors,
970
1001
  };
971
1002
  }
1003
+ case "security": {
1004
+ const subcommand = rest[0] as SecuritySubcommand | undefined;
1005
+ if (!subcommand || !SECURITY_SUBCOMMANDS.includes(subcommand)) {
1006
+ errors.push("forge security requires subcommand: prove");
1007
+ return { command: null, workspaceRoot, errors };
1008
+ }
1009
+ return {
1010
+ command: {
1011
+ kind: "security",
1012
+ subcommand,
1013
+ db: parseAdapterKind(parseOptionValue(argv, "--db")),
1014
+ databaseUrl: parseOptionValue(argv, "--database-url"),
1015
+ json: parseFlag(argv, "--json"),
1016
+ workspaceRoot,
1017
+ },
1018
+ workspaceRoot,
1019
+ errors,
1020
+ };
1021
+ }
972
1022
  case "auth": {
973
1023
  const subcommand = rest[0] as AuthSubcommand | undefined;
974
1024
  if (!subcommand || !AUTH_SUBCOMMANDS.includes(subcommand)) {
975
- errors.push("forge auth requires subcommand: check, config, decode, test-token, or jwks");
1025
+ errors.push("forge auth requires subcommand: check, config, decode, test-token, jwks, or prove");
976
1026
  return { command: null, workspaceRoot, errors };
977
1027
  }
978
1028
  return {
@@ -990,7 +1040,7 @@ export function parseCli(argv: string[]): ParsedCli {
990
1040
  case "rls": {
991
1041
  const subcommand = rest[0] as RlsSubcommand | undefined;
992
1042
  if (!subcommand || !RLS_SUBCOMMANDS.includes(subcommand)) {
993
- errors.push("forge rls requires subcommand: generate, check, apply, or test");
1043
+ errors.push("forge rls requires subcommand: generate, check, apply, test, or mutate-test");
994
1044
  return { command: null, workspaceRoot, errors };
995
1045
  }
996
1046
  return {
@@ -1082,6 +1132,8 @@ export function parseCli(argv: string[]): ParsedCli {
1082
1132
  ? undefined
1083
1133
  : primitive === "ui"
1084
1134
  ? rest[1] ?? "ui"
1135
+ : primitive === "ai-chat"
1136
+ ? rest[1] ?? "support"
1085
1137
  : rest[1];
1086
1138
  const explainPrimitive =
1087
1139
  primitive === "explain" ? (rest[1] as MakePrimitive | undefined) : undefined;
@@ -1092,7 +1144,7 @@ export function parseCli(argv: string[]): ParsedCli {
1092
1144
  errors.push("forge make explain requires a known primitive");
1093
1145
  }
1094
1146
  if (
1095
- !["list", "explain", "ui"].includes(primitive) &&
1147
+ !["list", "explain", "ui", "ai-chat"].includes(primitive) &&
1096
1148
  !name
1097
1149
  ) {
1098
1150
  errors.push(`forge make ${primitive} requires a name or plan id`);
@@ -1882,10 +1934,10 @@ export function parseCli(argv: string[]): ParsedCli {
1882
1934
  const subcommand = rest[0] as SecretsSubcommand | undefined;
1883
1935
  if (
1884
1936
  !subcommand ||
1885
- !["list", "check", "print", "set", "unset"].includes(subcommand)
1937
+ !["list", "check", "print", "set", "unset", "prove"].includes(subcommand)
1886
1938
  ) {
1887
1939
  errors.push(
1888
- "forge secrets requires subcommand: list, check, print, set, or unset",
1940
+ "forge secrets requires subcommand: list, check, print, set, unset, or prove",
1889
1941
  );
1890
1942
  return { command: null, workspaceRoot, errors };
1891
1943
  }
@@ -1925,13 +1977,17 @@ export function parseCli(argv: string[]): ParsedCli {
1925
1977
  }
1926
1978
  case "ai": {
1927
1979
  const subcommand = rest[0] as AiSubcommand | undefined;
1928
- if (!subcommand || !["providers", "check", "test", "models"].includes(subcommand)) {
1929
- errors.push("forge ai requires subcommand: providers, check, test, or models");
1980
+ if (!subcommand || !AI_SUBCOMMANDS.includes(subcommand)) {
1981
+ errors.push("forge ai requires subcommand: providers, check, test, models, tools, agents, redteam, or trace");
1930
1982
  return { command: null, workspaceRoot, errors };
1931
1983
  }
1932
1984
 
1933
1985
  const providerRaw = parseOptionValue(argv, "--provider");
1934
1986
  const provider = providerRaw as ForgeAiProvider | undefined;
1987
+ const traceId = subcommand === "trace" ? rest[1] ?? parseOptionValue(argv, "--trace") : undefined;
1988
+ if (subcommand === "trace" && !traceId) {
1989
+ errors.push("forge ai trace requires a trace id");
1990
+ }
1935
1991
 
1936
1992
  return {
1937
1993
  command: {
@@ -1942,6 +1998,9 @@ export function parseCli(argv: string[]): ParsedCli {
1942
1998
  model: parseOptionValue(argv, "--model"),
1943
1999
  prompt: parseOptionValue(argv, "--prompt"),
1944
2000
  mock: parseFlag(argv, "--mock"),
2001
+ traceId,
2002
+ db: parsePersistentDbKind(parseOptionValue(argv, "--db")),
2003
+ databaseUrl: parseOptionValue(argv, "--database-url"),
1945
2004
  workspaceRoot,
1946
2005
  },
1947
2006
  workspaceRoot,