forgeos 0.1.0-alpha.30 → 0.1.0-alpha.32
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 +3 -3
- package/CHANGELOG.md +30 -0
- package/docs/changelog.md +55 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +28 -2
- package/src/forge/cli/auth.ts +134 -9
- package/src/forge/cli/baseline.ts +112 -0
- package/src/forge/cli/changed.ts +52 -15
- package/src/forge/cli/commands.ts +117 -31
- package/src/forge/cli/db.ts +92 -4
- package/src/forge/cli/dev.ts +229 -44
- package/src/forge/cli/doctor.ts +81 -0
- package/src/forge/cli/handoff.ts +63 -5
- package/src/forge/cli/last-run.ts +84 -0
- package/src/forge/cli/main.ts +3 -1
- package/src/forge/cli/new.ts +130 -12
- package/src/forge/cli/output.ts +94 -12
- package/src/forge/cli/parse.ts +72 -10
- package/src/forge/cli/progress.ts +51 -0
- package/src/forge/cli/studio.ts +15 -14
- package/src/forge/cli/verify.ts +31 -0
- package/src/forge/cli/windows.ts +23 -0
- package/src/forge/cli/workos.ts +5 -4
- package/src/forge/compiler/agent-contract/build.ts +3 -3
- package/src/forge/compiler/data-graph/sql/ddl.ts +11 -1
- package/src/forge/compiler/diagnostics/codes.ts +4 -0
- package/src/forge/compiler/integration/templates/render.ts +1 -0
- package/src/forge/compiler/integration/templates/workos.ts +5 -5
- package/src/forge/compiler/make-registry/build.ts +2 -2
- package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
- package/src/forge/compiler/recipes/definitions.ts +1 -1
- package/src/forge/delta/status.ts +79 -12
- package/src/forge/dev/server.ts +4 -4
- package/src/forge/dev-console/cycle.ts +55 -20
- package/src/forge/dev-console/types.ts +1 -0
- package/src/forge/make/fields.ts +26 -0
- package/src/forge/make/index.ts +6 -1
- package/src/forge/runtime/db/factory.ts +1 -3
- package/src/forge/runtime/db/memory-adapter.ts +139 -32
- package/src/forge/runtime/db/pglite-adapter.ts +188 -2
- package/src/forge/ui/index.ts +174 -0
- package/src/forge/ui/types.ts +1 -0
- package/src/forge/version.ts +1 -1
- package/src/forge/workspace/baseline.ts +112 -0
- package/src/forge/workspace/change-summary.ts +8 -1
- package/src/forge/workspace/forge-cli.ts +29 -1
- package/src/forge/workspace/git-summary.ts +65 -2
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.32 input=26469f4d84697cd1397a4a6726dff71ec50e70270a82b3ea4c84c75f25ac5f58 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
|
@@ -205,8 +205,8 @@ node bin/forge.mjs do verify --json
|
|
|
205
205
|
Use:
|
|
206
206
|
|
|
207
207
|
```bash
|
|
208
|
-
node bin/forge.mjs make resource <name> --fields title:text,status:enum
|
|
209
|
-
node bin/forge.mjs make resource <name> --fields title:text,status:enum
|
|
208
|
+
node bin/forge.mjs make resource <name> --fields title:text,status:enum=open+closed --dry-run --json
|
|
209
|
+
node bin/forge.mjs make resource <name> --fields title:text,status:enum=open+closed --with-ui --yes
|
|
210
210
|
node bin/forge.mjs make ui --framework vite --dry-run --json
|
|
211
211
|
node bin/forge.mjs make ui --framework nuxt --dry-run --json
|
|
212
212
|
node bin/forge.mjs make ai-chat support --dry-run --json
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.32
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Harden the alpha field-test loop after real Codex, WorkOS, PGlite, and app-server exercises.
|
|
8
|
+
|
|
9
|
+
- Accept `create-forgeos-app --git` as a supported/no-op compatibility flag when git initialization is already handled by the scaffold path, and keep `npm create forgeos-app@alpha .` working from empty current directories.
|
|
10
|
+
- Reject schema definitions that try to model `id` as a normal text field so generated SQL cannot silently create the wrong primary-key shape.
|
|
11
|
+
- Fix tenant-scoped updates in the in-memory database adapter and bring memory timestamp behavior closer to PGlite by rejecting empty timestamp strings and returning `Date` objects for timestamp columns.
|
|
12
|
+
- Improve smoke/generate drift diagnostics and keep `forge handoff` read-only with respect to generated artifacts.
|
|
13
|
+
- Copy generated WorkOS seed data to a root-level `workos-seed.yml` for app DX, serve `HEAD /auth.md` and `HEAD /.well-known/oauth-protected-resource`, and make local-vs-production auth posture more explicit.
|
|
14
|
+
- Normalize suggested Forge commands in framework checkouts so `agent`, `delta`, `studio`, `status`, `changed`, `handoff`, doctors, and WorkOS/auth helpers recommend `node bin/forge.mjs ...` instead of a possibly stale global `forge`.
|
|
15
|
+
- Return structured JSON diagnostics for Delta/PGlite export/open failures instead of leaking raw PGlite stack output, and improve Windows/PGlite repair guidance.
|
|
16
|
+
- Expand `forge ui audit` with static UX/auth-readiness warnings for missing semantic landmarks, unlabeled form controls, unnamed buttons, missing loading/error/empty states, and tenant/prod-auth apps that still only show local dev auth posture.
|
|
17
|
+
|
|
18
|
+
## 0.1.0-alpha.31
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Smooth the ForgeOS field-demo DX after the alpha.30 app-server and WorkOS exercises.
|
|
23
|
+
|
|
24
|
+
- Add `forge baseline create` and `forge baseline status` so non-git template workspaces can establish a local baseline and get useful `forge changed` / `forge handoff` diffs instead of noisy filesystem inventories.
|
|
25
|
+
- Add `forge auth status` and production-focused `forge auth prove --prod` output so `dev-headers` is clearly labeled as local-only while JWT/OIDC proofs show production auth posture.
|
|
26
|
+
- Expand `forge doctor windows` with local PGlite store posture and cleanup guidance, including safer PGlite abort inspection that does not poison the surrounding process exit code.
|
|
27
|
+
- Add `forge ui audit` and run it during `forge verify --smoke` when a web app is present, catching missing UI scenarios, missing stable Forge test IDs, and missing policy-denied coverage for sensitive routes.
|
|
28
|
+
- Expand `forge ui audit` with static UX/auth-readiness warnings for missing semantic landmarks, unlabeled form controls, unnamed buttons, missing loading/error/empty states, and tenant/prod-auth apps that still only show local dev auth posture.
|
|
29
|
+
- Bring the in-memory DB adapter closer to PGlite for timestamp fields by rejecting empty timestamp values and returning `Date` objects for `timestamp`/`timestamptz` columns.
|
|
30
|
+
- Make `forge handoff` use a read-only generated-artifact check so preparing a handoff no longer rewrites `src/forge/_generated/**`, `forge.lock`, or generated `AGENTS.md` noise; `forge dev --once` still self-heals stale artifacts.
|
|
31
|
+
- Allow `forge new .` / `npm create forgeos-app@alpha .` from an empty current directory, while refusing non-empty directories to avoid overwriting existing apps.
|
|
32
|
+
|
|
3
33
|
## 0.1.0-alpha.30
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,61 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.32
|
|
10
|
+
|
|
11
|
+
- Accepted `create-forgeos-app --git` as a supported/no-op compatibility flag
|
|
12
|
+
when git initialization is already handled by the scaffold path, and kept
|
|
13
|
+
`npm create forgeos-app@alpha .` working from empty current directories.
|
|
14
|
+
- Added stronger schema validation for `id` so apps cannot accidentally model
|
|
15
|
+
the primary key as a normal text field and generate incorrect SQL.
|
|
16
|
+
- Fixed tenant-scoped updates in the in-memory database adapter and made memory
|
|
17
|
+
timestamp handling closer to PGlite by rejecting empty timestamp strings and
|
|
18
|
+
returning `Date` objects for timestamp columns.
|
|
19
|
+
- Improved smoke/generate drift diagnostics and kept `forge handoff` read-only
|
|
20
|
+
with respect to generated artifacts.
|
|
21
|
+
- Improved WorkOS/auth app DX: root-level `workos-seed.yml`, `HEAD /auth.md`,
|
|
22
|
+
`HEAD /.well-known/oauth-protected-resource`, and clearer local-vs-production
|
|
23
|
+
auth posture.
|
|
24
|
+
- Normalized suggested Forge commands in framework checkouts so `agent`,
|
|
25
|
+
`delta`, `studio`, `status`, `changed`, `handoff`, doctors, and WorkOS/auth
|
|
26
|
+
helpers recommend `node bin/forge.mjs ...` instead of a possibly stale global
|
|
27
|
+
`forge`.
|
|
28
|
+
- Returned structured JSON diagnostics for Delta/PGlite export/open failures
|
|
29
|
+
instead of raw PGlite stack output, and expanded Windows/PGlite repair hints.
|
|
30
|
+
- Expanded `forge ui audit` with static UX/auth-readiness warnings for missing
|
|
31
|
+
semantic landmarks, unlabeled form controls, unnamed buttons, missing
|
|
32
|
+
loading/error/empty states, and tenant/prod-auth apps that still only expose
|
|
33
|
+
local dev auth posture.
|
|
34
|
+
|
|
35
|
+
## 0.1.0-alpha.31
|
|
36
|
+
|
|
37
|
+
- Added Forge workspace baselines for non-git app directories:
|
|
38
|
+
`forge baseline create` records a local baseline and `forge changed` /
|
|
39
|
+
`forge handoff` can now report baseline diffs instead of noisy full
|
|
40
|
+
filesystem inventories.
|
|
41
|
+
- Added `forge auth status` and production-aware `forge auth prove --prod`
|
|
42
|
+
posture checks so local `dev-headers` auth is clearly distinguished from
|
|
43
|
+
JWT/OIDC production authentication.
|
|
44
|
+
- Expanded `forge doctor windows` with local PGlite store posture and cleanup
|
|
45
|
+
guidance, and made PGlite abort inspection preserve the surrounding process
|
|
46
|
+
exit code.
|
|
47
|
+
- Added `forge ui audit` and wired it into `forge verify --smoke` for web apps
|
|
48
|
+
to catch missing route scenarios, missing stable Forge test IDs, and missing
|
|
49
|
+
policy-denied coverage for sensitive flows. It now also reports static
|
|
50
|
+
UX/auth-readiness warnings for missing semantic landmarks, unlabeled form
|
|
51
|
+
controls, unnamed buttons, missing loading/error/empty states, and
|
|
52
|
+
tenant/prod-auth apps that still only expose local dev auth posture.
|
|
53
|
+
- Brought the in-memory DB adapter closer to PGlite for timestamp fields by
|
|
54
|
+
rejecting empty timestamp values and returning `Date` objects for
|
|
55
|
+
`timestamp`/`timestamptz` columns.
|
|
56
|
+
- Made `forge handoff` use a read-only generated-artifact check so preparing a
|
|
57
|
+
handoff no longer rewrites `src/forge/_generated/**`, `forge.lock`, or
|
|
58
|
+
generated `AGENTS.md` noise; `forge dev --once` still self-heals stale
|
|
59
|
+
artifacts.
|
|
60
|
+
- Allowed `forge new .` / `npm create forgeos-app@alpha .` from an empty
|
|
61
|
+
current directory, while refusing non-empty directories to avoid overwriting
|
|
62
|
+
existing apps.
|
|
63
|
+
|
|
9
64
|
## 0.1.0-alpha.30
|
|
10
65
|
|
|
11
66
|
- Hardened the WorkOS/AuthKit adapter and dev telemetry after the alpha.29
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.32","releaseId":"forgeos@0.1.0-alpha.32+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.32 input=26469f4d84697cd1397a4a6726dff71ec50e70270a82b3ea4c84c75f25ac5f58 content=ba652b73917e09bcc803cfea91f38e1aa2563b17eacc60f6faaebdc949997e9a
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.32",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.32+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from "../agent-memory/hook-runner.ts";
|
|
57
57
|
import { releaseManifest } from "../_generated/releaseManifest.ts";
|
|
58
58
|
import { runDevConsoleCycle } from "../dev-console/cycle.ts";
|
|
59
|
+
import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
|
|
59
60
|
|
|
60
61
|
export const AGENT_ADAPTER_VERSION = "agent-adapter-0.1.0";
|
|
61
62
|
export const AGENT_FORMAT_VERSION = "2026-06";
|
|
@@ -77,6 +78,25 @@ function diagnostic(
|
|
|
77
78
|
return createDiagnostic({ severity, code, message, ...(file ? { file } : {}) });
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
function normalizeAgentCliCommandHints<T>(workspaceRoot: string, result: T): T {
|
|
82
|
+
if (!result || typeof result !== "object") {
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
const value = result as Record<string, unknown>;
|
|
86
|
+
const normalized: Record<string, unknown> = { ...value };
|
|
87
|
+
for (const key of ["nextActions", "diagnostics", "commands", "checks", "steps"]) {
|
|
88
|
+
if (key in normalized) {
|
|
89
|
+
normalized[key] = normalizeForgeCliCommandsInValue(workspaceRoot, normalized[key], key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const key of ["installResult", "ingestResult"]) {
|
|
93
|
+
if (key in normalized) {
|
|
94
|
+
normalized[key] = normalizeAgentCliCommandHints(workspaceRoot, normalized[key]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return normalized as T;
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
function readText(workspaceRoot: string, relative: string): string | null {
|
|
81
101
|
const path = join(workspaceRoot, relative);
|
|
82
102
|
if (!nodeFileSystem.exists(path)) {
|
|
@@ -2222,9 +2242,11 @@ export async function runAgentHooksSmoke(options: AgentCommandOptions): Promise<
|
|
|
2222
2242
|
};
|
|
2223
2243
|
}
|
|
2224
2244
|
|
|
2225
|
-
|
|
2245
|
+
type AgentCommandResult =
|
|
2226
2246
|
AgentExportResult | AgentCheckResult | AgentTargetsResult | AgentPrintContextResult | AgentDoctorResult | AgentPrepareResult | AgentOnboardResult | AgentHooksSmokeResult | AgentHooksStatusResult | AgentTimelineResult | AgentMemoryCommandResult
|
|
2227
|
-
|
|
2247
|
+
;
|
|
2248
|
+
|
|
2249
|
+
async function runAgentCommandRaw(options: AgentCommandOptions): Promise<AgentCommandResult> {
|
|
2228
2250
|
if (options.subcommand === "list-targets") {
|
|
2229
2251
|
return runAgentListTargets(options.workspaceRoot);
|
|
2230
2252
|
}
|
|
@@ -2295,6 +2317,10 @@ export async function runAgentCommand(options: AgentCommandOptions): Promise<
|
|
|
2295
2317
|
};
|
|
2296
2318
|
}
|
|
2297
2319
|
|
|
2320
|
+
export async function runAgentCommand(options: AgentCommandOptions): Promise<AgentCommandResult> {
|
|
2321
|
+
return normalizeAgentCliCommandHints(options.workspaceRoot, await runAgentCommandRaw(options));
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2298
2324
|
export function formatAgentJson(result: Awaited<ReturnType<typeof runAgentCommand>>): string {
|
|
2299
2325
|
if ("timeline" in result && result.timeline === "agent") {
|
|
2300
2326
|
return `${JSON.stringify(result, null, 2)}\n`;
|
package/src/forge/cli/auth.ts
CHANGED
|
@@ -9,14 +9,19 @@ 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
|
import { loadSecretRegistry } from "../runtime/secrets/check.ts";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
|
|
12
15
|
|
|
13
|
-
export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove";
|
|
16
|
+
export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
|
|
14
17
|
|
|
15
18
|
export interface AuthCommandOptions {
|
|
16
19
|
subcommand: AuthSubcommand;
|
|
17
20
|
workspaceRoot: string;
|
|
18
21
|
json: boolean;
|
|
19
22
|
token?: string;
|
|
23
|
+
prod?: boolean;
|
|
24
|
+
scenario?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface AuthCommandResult {
|
|
@@ -59,11 +64,8 @@ function detectWorkOS(workspaceRoot: string, claims: AuthClaimsMapping) {
|
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
function
|
|
63
|
-
const config = loadAuthConfigFromEnv(workspaceRoot);
|
|
67
|
+
function configErrors(config: ReturnType<typeof loadAuthConfigFromEnv>): { code: string; message: string }[] {
|
|
64
68
|
const errors: { code: string; message: string }[] = [];
|
|
65
|
-
const workos = detectWorkOS(workspaceRoot, config.claims);
|
|
66
|
-
|
|
67
69
|
if ((config.mode === "jwt" || config.mode === "oidc") && !config.issuer) {
|
|
68
70
|
errors.push({
|
|
69
71
|
code: FORGE_AUTH_INVALID_ISSUER,
|
|
@@ -82,6 +84,99 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
|
|
|
82
84
|
message: "FORGE_AUTH_JWKS_URI is required for jwt auth",
|
|
83
85
|
});
|
|
84
86
|
}
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildAuthPosture(workspaceRoot: string) {
|
|
91
|
+
const config = loadAuthConfigFromEnv(workspaceRoot);
|
|
92
|
+
const productionMode = config.mode === "jwt" || config.mode === "oidc";
|
|
93
|
+
const errors = configErrors(config);
|
|
94
|
+
const configReady = errors.length === 0;
|
|
95
|
+
return normalizeForgeCliCommandsInValue(workspaceRoot, {
|
|
96
|
+
schemaVersion: "0.1.0",
|
|
97
|
+
mode: config.mode,
|
|
98
|
+
localOnly: config.mode === "dev-headers",
|
|
99
|
+
productionReady: productionMode && configReady,
|
|
100
|
+
requiresTenant: config.requiresTenant,
|
|
101
|
+
bearerHeader: productionMode ? "Authorization: Bearer <token>" : null,
|
|
102
|
+
tenantClaim: config.claims.tenantId ?? "tenant_id",
|
|
103
|
+
productionChecklist: [
|
|
104
|
+
{ item: "auth mode is jwt or oidc", ok: productionMode },
|
|
105
|
+
{ item: "FORGE_AUTH_ISSUER configured", ok: Boolean(config.issuer) },
|
|
106
|
+
{ item: "FORGE_AUTH_AUDIENCE configured", ok: Boolean(config.audience) },
|
|
107
|
+
{ item: "FORGE_AUTH_JWKS_URI or OIDC discovery configured", ok: config.mode === "oidc" || Boolean(config.jwksUri) },
|
|
108
|
+
{ item: "tenant claim mapped", ok: Boolean(config.claims.tenantId) },
|
|
109
|
+
{ item: "permission claim mapped", ok: Boolean(config.claims.permissions) },
|
|
110
|
+
{ item: "dev-headers disabled for public runtime", ok: config.mode !== "dev-headers" },
|
|
111
|
+
],
|
|
112
|
+
reason: productionMode
|
|
113
|
+
? configReady
|
|
114
|
+
? "jwt/oidc production auth configuration is present"
|
|
115
|
+
: "jwt/oidc production auth is selected but required settings are missing"
|
|
116
|
+
: "dev-headers auth is local-only and is not real production authentication",
|
|
117
|
+
nextActions: productionMode
|
|
118
|
+
? configReady
|
|
119
|
+
? ["forge auth prove --prod --token <jwt> --json", "forge serve --json"]
|
|
120
|
+
: ["forge auth check --json", "configure FORGE_AUTH_ISSUER, FORGE_AUTH_AUDIENCE, and FORGE_AUTH_JWKS_URI or OIDC issuer"]
|
|
121
|
+
: ["set FORGE_AUTH_MODE=jwt or oidc for production", "forge auth prove --prod --token <jwt> --json"],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
|
|
126
|
+
const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
|
|
127
|
+
const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
|
|
128
|
+
const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
|
|
129
|
+
const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
|
|
130
|
+
const permissions = ["onboarding:read", "invitations:create", "tasks:update"];
|
|
131
|
+
const permissionVocabularyPresent = workos.detected && workos.claimStatus.some((claim) => claim.name === "permissions" && claim.ok);
|
|
132
|
+
const checks = [
|
|
133
|
+
{
|
|
134
|
+
id: "tenant-claim",
|
|
135
|
+
ok: requiresTenant && workos.claimStatus.some((claim) => claim.name === "tenantId" && claim.ok),
|
|
136
|
+
evidence: "Forge tenant claim maps to WorkOS organization_id and tenant auth is required.",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "permission-claim",
|
|
140
|
+
ok: permissionVocabularyPresent,
|
|
141
|
+
evidence: "Forge permissions claim maps to WorkOS permissions.",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "seed-organizations",
|
|
145
|
+
ok: rootSeedPresent || generatedSeedPresent,
|
|
146
|
+
evidence: rootSeedPresent ? "workos-seed.yml exists at app root." : "generated WorkOS seed exists.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "agent-auth-metadata",
|
|
150
|
+
ok: authMdPresent && metadataPresent,
|
|
151
|
+
evidence: "public/auth.md and protected resource metadata are present.",
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
return normalizeForgeCliCommandsInValue(workspaceRoot, {
|
|
155
|
+
scenario: "multi-tenant",
|
|
156
|
+
ok: checks.every((check) => check.ok),
|
|
157
|
+
claims: {
|
|
158
|
+
acme: { organization_id: "org_acme", role: "owner", permissions },
|
|
159
|
+
globex: { organization_id: "org_globex", role: "member", permissions: ["onboarding:read", "tasks:update"] },
|
|
160
|
+
},
|
|
161
|
+
invariants: [
|
|
162
|
+
"Acme and Globex must use different organization_id claim values.",
|
|
163
|
+
"Tenant-scoped reads must include the active organization_id.",
|
|
164
|
+
"Tenant-scoped writes must verify the resource tenant before mutation.",
|
|
165
|
+
"Role-only UI affordances are not sufficient; policies must use permissions/claims.",
|
|
166
|
+
],
|
|
167
|
+
checks,
|
|
168
|
+
nextActions: [
|
|
169
|
+
"forge workos doctor --json",
|
|
170
|
+
"forge workos seed --file workos-seed.yml --dry-run --json",
|
|
171
|
+
"run an HTTP E2E with Acme and Globex tokens before production",
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validateConfig(workspaceRoot: string): AuthCommandResult {
|
|
177
|
+
const config = loadAuthConfigFromEnv(workspaceRoot);
|
|
178
|
+
const errors = configErrors(config);
|
|
179
|
+
const workos = detectWorkOS(workspaceRoot, config.claims);
|
|
85
180
|
|
|
86
181
|
return {
|
|
87
182
|
ok: errors.length === 0,
|
|
@@ -94,6 +189,7 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
|
|
|
94
189
|
algorithms: config.algorithms,
|
|
95
190
|
claims: config.claims,
|
|
96
191
|
requiresTenant: config.requiresTenant,
|
|
192
|
+
authPosture: buildAuthPosture(workspaceRoot),
|
|
97
193
|
workos,
|
|
98
194
|
errors,
|
|
99
195
|
},
|
|
@@ -116,6 +212,7 @@ function publicConfig(workspaceRoot: string): AuthCommandResult {
|
|
|
116
212
|
algorithms: config.algorithms,
|
|
117
213
|
claims: config.claims,
|
|
118
214
|
requiresTenant: config.requiresTenant,
|
|
215
|
+
authPosture: buildAuthPosture(workspaceRoot),
|
|
119
216
|
workos,
|
|
120
217
|
},
|
|
121
218
|
exitCode: 0,
|
|
@@ -197,6 +294,15 @@ async function testToken(
|
|
|
197
294
|
export async function runAuthCommand(
|
|
198
295
|
options: AuthCommandOptions,
|
|
199
296
|
): Promise<AuthCommandResult> {
|
|
297
|
+
if (options.subcommand === "status") {
|
|
298
|
+
const posture = buildAuthPosture(options.workspaceRoot);
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
mode: posture.mode,
|
|
302
|
+
data: posture,
|
|
303
|
+
exitCode: 0,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
200
306
|
if (options.subcommand === "check") {
|
|
201
307
|
return validateConfig(options.workspaceRoot);
|
|
202
308
|
}
|
|
@@ -206,15 +312,34 @@ export async function runAuthCommand(
|
|
|
206
312
|
const workos = detectWorkOS(options.workspaceRoot, config.claims);
|
|
207
313
|
const productionMode = config.mode === "jwt" || config.mode === "oidc";
|
|
208
314
|
const workosClaimsOk = workos.claimStatus.every((claim) => claim.ok);
|
|
315
|
+
const posture = buildAuthPosture(options.workspaceRoot);
|
|
316
|
+
const tokenProof = options.token ? await testToken(options.workspaceRoot, options.token) : null;
|
|
317
|
+
const prodError = options.prod && !productionMode
|
|
318
|
+
? { code: "FORGE_AUTH_MODE_INVALID", message: "forge auth prove --prod requires FORGE_AUTH_MODE=jwt or oidc" }
|
|
319
|
+
: options.prod && !options.token
|
|
320
|
+
? { code: "FORGE_AUTH_MISSING_TOKEN", message: "forge auth prove --prod requires --token" }
|
|
321
|
+
: options.prod && tokenProof && !tokenProof.ok
|
|
322
|
+
? tokenProof.error
|
|
323
|
+
: undefined;
|
|
324
|
+
const proofOk = checked.ok && (!options.prod || (productionMode && tokenProof?.ok === true));
|
|
325
|
+
const multiTenantProof = options.scenario === "multi-tenant"
|
|
326
|
+
? buildMultiTenantProof(options.workspaceRoot, workos, config.requiresTenant)
|
|
327
|
+
: null;
|
|
328
|
+
const scenarioOk = !multiTenantProof || multiTenantProof.ok;
|
|
209
329
|
return {
|
|
210
|
-
ok:
|
|
330
|
+
ok: proofOk && scenarioOk,
|
|
211
331
|
mode: config.mode,
|
|
212
332
|
data: {
|
|
213
333
|
schemaVersion: "0.1.0",
|
|
214
334
|
kind: "auth-proof",
|
|
215
|
-
ok:
|
|
335
|
+
ok: proofOk,
|
|
216
336
|
mode: config.mode,
|
|
217
337
|
productionReady: productionMode && checked.ok,
|
|
338
|
+
prod: options.prod === true,
|
|
339
|
+
scenario: options.scenario ?? null,
|
|
340
|
+
authPosture: posture,
|
|
341
|
+
...(tokenProof ? { tokenProof: tokenProof.ok ? tokenProof.data : tokenProof.error } : {}),
|
|
342
|
+
...(multiTenantProof ? { multiTenantProof } : {}),
|
|
218
343
|
invariants: [
|
|
219
344
|
{
|
|
220
345
|
id: "INV-001",
|
|
@@ -249,8 +374,8 @@ export async function runAuthCommand(
|
|
|
249
374
|
workos,
|
|
250
375
|
checkedAt: "deterministic",
|
|
251
376
|
},
|
|
252
|
-
error: checked.error,
|
|
253
|
-
exitCode:
|
|
377
|
+
error: prodError ?? checked.error,
|
|
378
|
+
exitCode: proofOk && scenarioOk ? 0 : 1,
|
|
254
379
|
};
|
|
255
380
|
}
|
|
256
381
|
if (options.subcommand === "config") {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createWorkspaceBaseline, readWorkspaceBaseline, writeWorkspaceBaseline, WORKSPACE_BASELINE_PATH } from "../workspace/baseline.ts";
|
|
3
|
+
import { listWorkspaceFiles } from "../workspace/git-summary.ts";
|
|
4
|
+
|
|
5
|
+
export type BaselineSubcommand = "create" | "status";
|
|
6
|
+
|
|
7
|
+
export interface BaselineCommandOptions {
|
|
8
|
+
subcommand: BaselineSubcommand;
|
|
9
|
+
workspaceRoot: string;
|
|
10
|
+
json: boolean;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BaselineCommandResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
path: string;
|
|
17
|
+
required: boolean;
|
|
18
|
+
created?: boolean;
|
|
19
|
+
baseline?: ReturnType<typeof readWorkspaceBaseline>;
|
|
20
|
+
summary: {
|
|
21
|
+
files: number;
|
|
22
|
+
reason?: string;
|
|
23
|
+
tracking: "git" | "forge-baseline" | "missing";
|
|
24
|
+
};
|
|
25
|
+
nextActions: string[];
|
|
26
|
+
exitCode: 0 | 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function gitAvailable(workspaceRoot: string): boolean {
|
|
30
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
31
|
+
cwd: workspaceRoot,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
});
|
|
35
|
+
return result.status === 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function runBaselineCommand(options: BaselineCommandOptions): BaselineCommandResult {
|
|
39
|
+
if (options.subcommand === "create") {
|
|
40
|
+
const files = listWorkspaceFiles(options.workspaceRoot);
|
|
41
|
+
const baseline = createWorkspaceBaseline({
|
|
42
|
+
workspaceRoot: options.workspaceRoot,
|
|
43
|
+
files,
|
|
44
|
+
reason: options.reason,
|
|
45
|
+
});
|
|
46
|
+
writeWorkspaceBaseline(options.workspaceRoot, baseline);
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
path: WORKSPACE_BASELINE_PATH,
|
|
50
|
+
required: false,
|
|
51
|
+
created: true,
|
|
52
|
+
baseline,
|
|
53
|
+
summary: {
|
|
54
|
+
files: Object.keys(baseline.files).length,
|
|
55
|
+
tracking: "forge-baseline",
|
|
56
|
+
...(baseline.reason ? { reason: baseline.reason } : {}),
|
|
57
|
+
},
|
|
58
|
+
nextActions: ["forge changed --json", "forge handoff --json"],
|
|
59
|
+
exitCode: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const baseline = readWorkspaceBaseline(options.workspaceRoot);
|
|
64
|
+
if (!baseline && gitAvailable(options.workspaceRoot)) {
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
path: WORKSPACE_BASELINE_PATH,
|
|
68
|
+
required: false,
|
|
69
|
+
baseline: null,
|
|
70
|
+
summary: {
|
|
71
|
+
files: 0,
|
|
72
|
+
tracking: "git",
|
|
73
|
+
},
|
|
74
|
+
nextActions: ["forge changed --json", "git status --short"],
|
|
75
|
+
exitCode: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: baseline !== null,
|
|
80
|
+
path: WORKSPACE_BASELINE_PATH,
|
|
81
|
+
required: baseline === null,
|
|
82
|
+
baseline,
|
|
83
|
+
summary: {
|
|
84
|
+
files: baseline ? Object.keys(baseline.files).length : 0,
|
|
85
|
+
tracking: baseline ? "forge-baseline" : "missing",
|
|
86
|
+
...(baseline?.reason ? { reason: baseline.reason } : {}),
|
|
87
|
+
},
|
|
88
|
+
nextActions: baseline
|
|
89
|
+
? ["forge changed --json", "forge baseline create --reason refresh --json"]
|
|
90
|
+
: ["forge baseline create --reason initial-scaffold --json", "git init"],
|
|
91
|
+
exitCode: baseline ? 0 : 1,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatBaselineJson(result: BaselineCommandResult): string {
|
|
96
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatBaselineHuman(result: BaselineCommandResult): string {
|
|
100
|
+
const label = result.required ? "missing" : result.summary.tracking === "git" ? "not required (git workspace)" : "ready";
|
|
101
|
+
const lines = [
|
|
102
|
+
`Forge baseline: ${label}`,
|
|
103
|
+
`Path: ${result.path}`,
|
|
104
|
+
`Tracking: ${result.summary.tracking}`,
|
|
105
|
+
`Files: ${result.summary.files}`,
|
|
106
|
+
];
|
|
107
|
+
if (result.summary.reason) {
|
|
108
|
+
lines.push(`Reason: ${result.summary.reason}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push("", "Next:", ...result.nextActions.map((action) => ` ${action}`));
|
|
111
|
+
return `${lines.join("\n")}\n`;
|
|
112
|
+
}
|