swallowkit 1.0.0-beta.58 → 1.0.0-beta.59

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 (58) hide show
  1. package/dist/cli/commands/add-auth.d.ts +3 -1
  2. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  3. package/dist/cli/commands/add-auth.js +55 -21
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/core/operations/auth-operations.d.ts +4 -0
  9. package/dist/core/operations/auth-operations.d.ts.map +1 -1
  10. package/dist/core/operations/auth-operations.js +5 -2
  11. package/dist/core/operations/auth-operations.js.map +1 -1
  12. package/dist/core/operations/create-model.d.ts.map +1 -1
  13. package/dist/core/operations/create-model.js +6 -2
  14. package/dist/core/operations/create-model.js.map +1 -1
  15. package/dist/core/project/capabilities.d.ts +58 -0
  16. package/dist/core/project/capabilities.d.ts.map +1 -0
  17. package/dist/core/project/capabilities.js +135 -0
  18. package/dist/core/project/capabilities.js.map +1 -0
  19. package/dist/core/project/validation.d.ts.map +1 -1
  20. package/dist/core/project/validation.js +48 -0
  21. package/dist/core/project/validation.js.map +1 -1
  22. package/dist/core/project/workflows.d.ts.map +1 -1
  23. package/dist/core/project/workflows.js +46 -2
  24. package/dist/core/project/workflows.js.map +1 -1
  25. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  26. package/dist/core/scaffold/auth-generator.js +6 -2
  27. package/dist/core/scaffold/auth-generator.js.map +1 -1
  28. package/dist/core/verify/index.d.ts +7 -0
  29. package/dist/core/verify/index.d.ts.map +1 -1
  30. package/dist/core/verify/index.js +69 -0
  31. package/dist/core/verify/index.js.map +1 -1
  32. package/dist/machine/index.d.ts.map +1 -1
  33. package/dist/machine/index.js +117 -41
  34. package/dist/machine/index.js.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +32 -10
  37. package/dist/mcp/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/__tests__/agent-loop-phase34.test.ts +66 -2
  40. package/src/__tests__/auth.test.ts +9 -0
  41. package/src/__tests__/machine.test.ts +15 -0
  42. package/src/__tests__/multi-auth.test.ts +12 -0
  43. package/src/__tests__/plan-apply.test.ts +38 -0
  44. package/src/cli/commands/add-auth.ts +59 -21
  45. package/src/cli/index.ts +2 -0
  46. package/src/core/operations/auth-operations.ts +9 -2
  47. package/src/core/operations/create-model.ts +7 -2
  48. package/src/core/project/agent-templates/AGENTS.md +62 -2
  49. package/src/core/project/agent-templates/CLAUDE.md +3 -0
  50. package/src/core/project/agent-templates/copilot-instructions.md +3 -0
  51. package/src/core/project/agent-templates/shared-models.instructions.md +39 -2
  52. package/src/core/project/capabilities.ts +193 -0
  53. package/src/core/project/validation.ts +50 -0
  54. package/src/core/project/workflows.ts +48 -2
  55. package/src/core/scaffold/auth-generator.ts +7 -2
  56. package/src/core/verify/index.ts +76 -0
  57. package/src/machine/index.ts +133 -45
  58. package/src/mcp/index.ts +28 -10
@@ -67,8 +67,14 @@ Use only the inspections relevant to the task. Typical inspections include:
67
67
  {{runCmd}} swallowkit machine inspect boundaries
68
68
  {{runCmd}} swallowkit machine inspect drift
69
69
  {{runCmd}} swallowkit machine inspect infra
70
+ {{runCmd}} swallowkit machine inspect capabilities
70
71
  ```
71
72
 
73
+ `inspect capabilities` answers "what can SwallowKit do" (supported model
74
+ declarations with exact formats, the correct auth-introduction workflow, what
75
+ generated CRUD does and does not guarantee, how seeds are applied). Use it
76
+ instead of guessing or grepping SwallowKit internals.
77
+
72
78
  Use the equivalent `swallowkit_*` MCP tools when available.
73
79
 
74
80
  Questions that can be answered from the repository or SwallowKit inspection
@@ -236,6 +242,16 @@ Otherwise read and follow the equivalent runbook under
236
242
 
237
243
  Never repair schema drift by directly patching SwallowKit-managed artifacts.
238
244
 
245
+ Additional model-change facts:
246
+
247
+ - The shared package is consumed through its built `dist/`. `verify project`
248
+ rebuilds it automatically before typecheck; when typechecking manually,
249
+ build the shared package first (stale `dist/` shows up as
250
+ "no exported member" errors).
251
+ - Seed JSON under `dev-seeds/` is only applied by
252
+ `{{runCmd}} swallowkit dev --seed-env <environment>`; editing the files
253
+ alone does nothing.
254
+
239
255
  ## New Models / CRUD Features
240
256
 
241
257
  Use the `swallowkit-add-model` Agent Skill when available.
@@ -247,6 +263,43 @@ SwallowKit should generate framework-owned CRUD artifacts.
247
263
 
248
264
  Do not manually recreate boilerplate that SwallowKit owns.
249
265
 
266
+ ## Authentication
267
+
268
+ Authentication is a **plan/apply operation**, not a free-form config edit.
269
+ Ownership of `swallowkit.config` is split:
270
+
271
+ - `auth.schemes` — owned by SwallowKit. **Always start with
272
+ `{{runCmd}} swallowkit machine plan auth --provider <p> [--scheme <name>]`.
273
+ Never hand-write `auth.schemes` entries**; apply auth appends them, and a
274
+ hand-written scheme makes the plan fail with "already exists".
275
+ - `auth.authorization.policies` and `swa.allowedProviders` — hand-edited
276
+ **after** apply (the config is an extension point; this edit is expected).
277
+ `swa.allowedProviders` can also be set at plan time with
278
+ `--allowed-providers github,aad`.
279
+
280
+ After `apply auth`, follow the returned `nextActions`:
281
+
282
+ 1. Define `auth.authorization.policies` referencing the new scheme.
283
+ 2. For `swa`: confirm `swa.allowedProviders`; generated login URLs use it.
284
+ 3. For `external-token`: implement the generated verifier stub. It fails
285
+ closed, and **verify passes with the stub in place** — a green verify does
286
+ not mean external-token auth works end-to-end (`verify project` surfaces a
287
+ `auth-verifier-stub` warning while the stub is unmodified).
288
+ 4. Re-run plan/apply scaffold for models that need auth guards, then verify.
289
+
290
+ Use the `swallowkit-add-auth` Agent Skill or the
291
+ `.swallowkit/workflows/add-auth.md` runbook when available.
292
+
293
+ ### Generated CRUD scope
294
+
295
+ Generated CRUD endpoints enforce the model's `authPolicy`
296
+ (authentication + roles) — **they do NOT scope data to the authenticated
297
+ user**. Any caller passing the policy can read or write any document. For
298
+ multi-user data where users may only touch their own records, add ai-authored
299
+ custom endpoints that derive the scoping key (e.g. `learnerId`) from the
300
+ verified principal — never from the request body — and restrict the generated
301
+ write endpoints to admin roles.
302
+
250
303
  ## Verification Is a Completion Gate
251
304
 
252
305
  Before treating an agent-driven implementation as complete, run SwallowKit
@@ -318,6 +371,7 @@ Current workflows include:
318
371
 
319
372
  - `swallowkit-add-model`
320
373
  - `swallowkit-modify-model`
374
+ - `swallowkit-add-auth`
321
375
  - `swallowkit-verify-repair`
322
376
  - `swallowkit-provision`
323
377
 
@@ -400,9 +454,15 @@ same generated structure.
400
454
  - The Zod schema constant and inferred TypeScript type use the same PascalCase
401
455
  name: `export const Todo = z.object(...); export type Todo = z.infer<typeof Todo>`.
402
456
  - Re-export models from `shared/index.ts`.
403
- - Mark backend-managed fields as optional in source-of-truth schemas.
457
+ - `id` is a required `z.string()`; only `createdAt` / `updatedAt` are optional
458
+ backend-managed fields.
459
+ - Models may declare `partitionKey`, `authPolicy`, `displayName`, and
460
+ `connectorConfig` as exported constants. Exact formats:
461
+ `{{runCmd}} swallowkit machine inspect capabilities` (modelDeclarations).
404
462
  - Cosmos DB containers use PascalCase plural names and default to `/id` as the
405
- partition key unless the model declares another key.
463
+ partition key unless the model declares another key, e.g.
464
+ `export const partitionKey = '/learnerId';` (the field must exist in the
465
+ schema).
406
466
 
407
467
  ## Everyday Commands
408
468
 
@@ -17,6 +17,9 @@ Always-on rules:
17
17
  - Fall back to `{{runCmd}} swallowkit machine ...` when MCP is unavailable.
18
18
  - Inspect responsibility boundaries before editing framework-related files.
19
19
  - Never hand-edit deterministic SwallowKit-managed artifacts.
20
+ - Add auth schemes only via plan/apply auth; never hand-write `auth.schemes`
21
+ in swallowkit.config (policies and `swa.allowedProviders` are hand-edited
22
+ after apply).
20
23
  - Use plan/apply for deterministic generation.
21
24
  - Respect all `requires-human` states and provisioning approval gates.
22
25
  - Run SwallowKit verification before considering implementation complete.
@@ -16,6 +16,9 @@ Key rules:
16
16
  - Otherwise use `{{runCmd}} swallowkit machine ...`.
17
17
  - Inspect responsibility boundaries before editing framework-related files.
18
18
  - Never hand-edit deterministic SwallowKit-managed artifacts.
19
+ - Add auth schemes only via plan/apply auth; never hand-write `auth.schemes`
20
+ in swallowkit.config (policies and `swa.allowedProviders` are hand-edited
21
+ after apply).
19
22
  - Use plan/apply for deterministic generation.
20
23
  - Respect `requires-human` states and other human approval gates.
21
24
  - Run SwallowKit verification before completion.
@@ -13,15 +13,52 @@ source-of-truth models, subject to the current result of
13
13
  - Define Zod schemas using `zod/v4` (`import { z } from 'zod/v4'`).
14
14
  - Use the Zod pattern where the schema constant and inferred TypeScript type
15
15
  share the same name.
16
- - Include `id`, `createdAt`, and `updatedAt` as optional string fields. The
17
- backend manages their values.
16
+ - `id` is a **required** string field: `id: z.string()`. Generated CRUD, UI,
17
+ and repositories assume `id` is always present — do NOT make it optional.
18
+ - `createdAt` and `updatedAt` are **optional** string fields
19
+ (`z.string().optional()`). The backend manages their values.
18
20
  - Export a `displayName` string for UI display.
19
21
  - Re-export every model from `shared/index.ts`, outside managed markers where
20
22
  applicable.
21
23
  - For relationships, use nested schemas rather than duplicating model shapes.
22
24
 
25
+ ## Optional model declarations (exact formats)
26
+
27
+ These are the only declarations SwallowKit parses from a model file:
28
+
29
+ ```ts
30
+ // Cosmos DB partition key (default '/id'). Must reference a schema field.
31
+ export const partitionKey = '/learnerId';
32
+
33
+ // Role-based access control on generated CRUD.
34
+ export const authPolicy = { roles: ['admin'] };
35
+ // or: { read: ['user'], write: ['admin'] } or: { policy: 'adminOnly' }
36
+
37
+ // Human-readable name for generated UI.
38
+ export const displayName = 'Learner';
39
+
40
+ // Bind the model to a configured external connector instead of Cosmos DB.
41
+ export const connectorConfig = {
42
+ connector: 'mysql',
43
+ table: 'learners',
44
+ operations: ['list', 'get', 'create', 'update', 'delete'],
45
+ };
46
+ ```
47
+
48
+ The full machine-readable contract:
49
+ `{{runCmd}} swallowkit machine inspect capabilities`
50
+
23
51
  After changing a model, do not manually patch generated artifacts.
24
52
 
53
+ ## After changing a model
54
+
55
+ - The shared package is consumed via its built `dist/`. `verify project`
56
+ rebuilds it automatically, but if you typecheck manually, build the shared
57
+ package first — "no exported member" errors usually mean a stale `dist/`.
58
+ - If `dev-seeds/` exists, update the seed JSON. Seeds are only applied by
59
+ running `{{runCmd}} swallowkit dev --seed-env <environment>`; editing the
60
+ JSON alone does nothing.
61
+
25
62
  Use the `swallowkit-modify-model` Agent Skill when available. Otherwise follow
26
63
  `.swallowkit/workflows/modify-model.md`.
27
64
 
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Capabilities — 「SwallowKit に何ができるか」の機械可読な自己記述。
3
+ *
4
+ * inspect project/boundaries/infra が「今の状態」を返すのに対し、
5
+ * inspect capabilities は「モデルで宣言できるもの・認証の正しい導入手順・
6
+ * 生成 CRUD の守備範囲・シード適用方法」など、状態に依存しない
7
+ * フレームワーク仕様を返す。Coding Agent がドキュメント外の知識を
8
+ * 推測で埋めるのを構造的に防ぐことが目的。
9
+ */
10
+
11
+ import { getSwallowKitVersion } from "../../version";
12
+
13
+ export interface CapabilityModelDeclaration {
14
+ name: string;
15
+ required: boolean;
16
+ description: string;
17
+ format: string;
18
+ notes?: string[];
19
+ }
20
+
21
+ export interface CapabilityAuthProvider {
22
+ provider: string;
23
+ description: string;
24
+ postApplySteps: string[];
25
+ }
26
+
27
+ export interface CapabilitiesContract {
28
+ contractVersion: number;
29
+ swallowkitVersion: string;
30
+ modelDeclarations: CapabilityModelDeclaration[];
31
+ authentication: {
32
+ correctWorkflow: string[];
33
+ rules: string[];
34
+ providers: CapabilityAuthProvider[];
35
+ authorizationPolicies: {
36
+ semantics: string;
37
+ configExample: string;
38
+ handEditAllowed: boolean;
39
+ };
40
+ };
41
+ generatedCrud: {
42
+ guarantees: string[];
43
+ notGuaranteed: string[];
44
+ guidance: string[];
45
+ };
46
+ sharedModels: {
47
+ idFieldRule: string;
48
+ buildNote: string;
49
+ };
50
+ seeding: {
51
+ createTemplates: string;
52
+ applySeeds: string;
53
+ notes: string[];
54
+ };
55
+ machineCommands: string[];
56
+ verifyChecks: {
57
+ defaultChecks: string[];
58
+ optInChecks: string[];
59
+ customChecks: string;
60
+ };
61
+ }
62
+
63
+ export function inspectCapabilities(): CapabilitiesContract {
64
+ return {
65
+ contractVersion: 1,
66
+ swallowkitVersion: getSwallowKitVersion(),
67
+ modelDeclarations: [
68
+ {
69
+ name: "schema",
70
+ required: true,
71
+ description:
72
+ "Zod object schema; the schema constant and the inferred type share the same PascalCase name. `id` MUST be a required string — generated CRUD, UI, and repositories assume it is always present. Only `createdAt` / `updatedAt` are optional (backend-managed).",
73
+ format:
74
+ "export const Todo = z.object({ id: z.string(), title: z.string().min(1), createdAt: z.string().optional(), updatedAt: z.string().optional() });\nexport type Todo = z.infer<typeof Todo>;",
75
+ },
76
+ {
77
+ name: "displayName",
78
+ required: false,
79
+ description: "Human-readable name used by generated UI. Defaults to the model name.",
80
+ format: "export const displayName = 'Todo';",
81
+ },
82
+ {
83
+ name: "partitionKey",
84
+ required: false,
85
+ description:
86
+ "Cosmos DB partition key path for the generated container. Must start with '/' and reference a field that exists in the schema. Defaults to '/id'.",
87
+ format: "export const partitionKey = '/learnerId';",
88
+ notes: ["The referenced field (e.g. learnerId) must be declared in the Zod schema."],
89
+ },
90
+ {
91
+ name: "authPolicy",
92
+ required: false,
93
+ description:
94
+ "Role-based access control metadata applied to the generated CRUD Functions. `roles` guards all operations; `read`/`write` guard per-operation; `policy` references a named authorization policy from swallowkit.config auth.authorization.policies.",
95
+ format:
96
+ "export const authPolicy = { roles: ['admin'] };\n// or per-operation:\nexport const authPolicy = { read: ['user'], write: ['admin'] };\n// or a named policy:\nexport const authPolicy = { policy: 'adminOnly' };",
97
+ },
98
+ {
99
+ name: "connectorConfig",
100
+ required: false,
101
+ description:
102
+ "Binds the model to a configured external connector (RDB / API) instead of Cosmos DB. The connector must exist in swallowkit.config connectors.",
103
+ format:
104
+ "export const connectorConfig = {\n connector: 'mysql',\n table: 'todos',\n operations: ['list', 'get', 'create', 'update', 'delete'],\n};",
105
+ },
106
+ ],
107
+ authentication: {
108
+ correctWorkflow: [
109
+ "1. plan auth --provider <custom-jwt|swa|external-token> [--scheme <name>] [--allowed-providers <csv>] — computes the change plan; SwallowKit writes auth.schemes into swallowkit.config for you.",
110
+ "2. apply auth --plan <planId> — generates auth code and appends the scheme to swallowkit.config.",
111
+ "3. Hand-edit swallowkit.config auth.authorization.policies to define named policies (the config is an extension point; this edit is expected).",
112
+ "4. For swa schemes: set swa.allowedProviders via --allowed-providers at plan time, or hand-edit it in the config after apply.",
113
+ "5. For external-token schemes: implement the generated verifier stub — it fails closed (throws) until implemented.",
114
+ "6. Re-run plan/apply scaffold for models that need auth guards, then verify project.",
115
+ ],
116
+ rules: [
117
+ "NEVER hand-write auth.schemes entries in swallowkit.config — plan/apply auth owns them and duplicate names abort with 'already exists'.",
118
+ "authorization.policies and swa.allowedProviders ARE hand-edited after apply (extension-point edits).",
119
+ "Verifier stubs pass typecheck and verify; a passing verify does NOT mean external-token auth works end-to-end.",
120
+ ],
121
+ providers: [
122
+ {
123
+ provider: "custom-jwt",
124
+ description: "Login/logout/me endpoints backed by an RDB user table, bcrypt password check, HS256 JWT.",
125
+ postApplySteps: ["Set JWT_SECRET in functions/local.settings.json.", "Ensure the user table matches auth.customJwt config."],
126
+ },
127
+ {
128
+ provider: "swa",
129
+ description: "Azure Static Web Apps built-in auth (EasyAuth). Principal arrives via x-ms-client-principal.",
130
+ postApplySteps: [
131
+ "Set swa.allowedProviders (e.g. ['github']) via --allowed-providers or by hand-editing the config; generated login URLs use the first entry.",
132
+ ],
133
+ },
134
+ {
135
+ provider: "external-token",
136
+ description: "Bearer tokens issued by an external IdP (LINE, Auth0, ...). SwallowKit generates a fail-closed verifier stub.",
137
+ postApplySteps: [
138
+ "Implement the generated verifier (extension point / generated-once; edit freely).",
139
+ "Implement the frontend token adapter (lib/auth/**/token-adapter.ts).",
140
+ ],
141
+ },
142
+ {
143
+ provider: "none",
144
+ description: "Disables authentication in the config.",
145
+ postApplySteps: [],
146
+ },
147
+ ],
148
+ authorizationPolicies: {
149
+ semantics:
150
+ "A named policy lists which schemes may satisfy it and which roles are required. Generated guards call requireAuth(request, policyName). Exactly one scheme must match the presented credentials.",
151
+ configExample:
152
+ "auth: {\n schemes: { admin: { provider: 'swa', swa: { allowedProviders: ['github'] } }, api: { provider: 'external-token' } },\n authorization: {\n defaultPolicy: 'anonymous',\n policies: {\n adminOnly: { schemes: ['admin'], roles: ['administrator'] },\n learner: { schemes: ['api'] },\n },\n },\n}",
153
+ handEditAllowed: true,
154
+ },
155
+ },
156
+ generatedCrud: {
157
+ guarantees: [
158
+ "Generated CRUD endpoints enforce the model's authPolicy (authentication + role checks).",
159
+ "Zod validation runs before persistence.",
160
+ ],
161
+ notGuaranteed: [
162
+ "Owner scoping: generated CRUD does NOT restrict rows to the authenticated principal. Any authenticated caller passing the policy can read/write any document.",
163
+ ],
164
+ guidance: [
165
+ "For multi-user data where users may only touch their own records, add ai-authored custom endpoints that derive the scoping key (e.g. learnerId) from the verified principal — never from the request body — and restrict generated write endpoints to admin roles or disable them.",
166
+ ],
167
+ },
168
+ sharedModels: {
169
+ idFieldRule: "id: z.string() (required). createdAt/updatedAt: z.string().optional(). The backend owns their values.",
170
+ buildNote:
171
+ "verify typecheck resolves the shared package from its built dist/. SwallowKit rebuilds shared automatically during verify; when typechecking manually, build the shared package first (errors like \"no exported member\" usually mean a stale dist).",
172
+ },
173
+ seeding: {
174
+ createTemplates: "swallowkit create-dev-seeds <environment> — writes dev-seeds/<environment>/<model>.json templates.",
175
+ applySeeds: "swallowkit dev --seed-env <environment> — applies the seed JSON to the local Cosmos DB emulator at dev startup.",
176
+ notes: ["Editing dev-seeds/*.json alone does nothing; seeds are only applied via dev --seed-env."],
177
+ },
178
+ machineCommands: [
179
+ "inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities",
180
+ "validate project",
181
+ "generate model <names...> | generate scaffold <model>",
182
+ "plan scaffold <models...> | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc>",
183
+ "apply scaffold [models...] --plan <id> [--approve] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve",
184
+ "verify project [--checks <ids>] [--compact]",
185
+ "explain failure [--check <id>]",
186
+ ],
187
+ verifyChecks: {
188
+ defaultChecks: ["structure", "drift", "typecheck"],
189
+ optInChecks: ["build", "lint", "test"],
190
+ customChecks: "swallowkit.config verify.checks: [{ id, command, title?, timeoutMs? }]",
191
+ },
192
+ };
193
+ }
@@ -192,6 +192,55 @@ function walkFiles(directory: string, extensions: string[]): string[] {
192
192
  return collected;
193
193
  }
194
194
 
195
+ const VERIFIER_STUB_MARKER = /External token verifier(?: for \S+)? is not configured/;
196
+
197
+ /**
198
+ * external-token スキームの verifier が生成スタブのまま (fail-closed throw のみ) だと
199
+ * typecheck / verify は通るのに実行時は必ず 401/503 になる。
200
+ * 「認証は生成されたが動かない」状態を warning として可視化する。
201
+ */
202
+ function validateExternalTokenVerifierStubs(projectRoot: string, manifest: ProjectManifest, violations: ProjectViolation[]): void {
203
+ const auth = manifest.auth;
204
+ const hasExternalToken =
205
+ auth?.provider === "external-token" ||
206
+ Object.values(auth?.schemes ?? {}).some((scheme) => scheme.provider === "external-token");
207
+ if (!hasExternalToken) return;
208
+
209
+ const candidates: string[] = [
210
+ path.join("functions", "src", "auth", "external-token-verifier.ts"),
211
+ path.join("functions", "Auth", "ExternalTokenVerifier.cs"),
212
+ path.join("functions", "auth", "external_token_verifier.py"),
213
+ ];
214
+ const schemeDirs = [
215
+ { root: path.join("functions", "src", "auth", "schemes"), file: "verifier.ts" },
216
+ { root: path.join("functions", "Auth", "Schemes"), file: "ExternalTokenVerifier.cs" },
217
+ { root: path.join("functions", "auth", "schemes"), file: "verifier.py" },
218
+ ];
219
+ for (const { root, file } of schemeDirs) {
220
+ const absoluteRoot = path.join(projectRoot, root);
221
+ if (!fs.existsSync(absoluteRoot)) continue;
222
+ for (const entry of fs.readdirSync(absoluteRoot, { withFileTypes: true })) {
223
+ if (entry.isDirectory()) candidates.push(path.join(root, entry.name, file));
224
+ }
225
+ }
226
+
227
+ for (const relativePath of candidates) {
228
+ const absolutePath = path.join(projectRoot, relativePath);
229
+ if (!fs.existsSync(absolutePath)) continue;
230
+ const content = fs.readFileSync(absolutePath, "utf-8");
231
+ if (VERIFIER_STUB_MARKER.test(content)) {
232
+ pushViolation(violations, {
233
+ code: "auth-verifier-stub",
234
+ severity: "warning",
235
+ message:
236
+ "External token verifier is still the generated stub; it fails closed, so external-token authentication cannot succeed at runtime.",
237
+ location: { path: relativePath.replace(/\\/g, "/") },
238
+ suggestedFix: "Implement the verifier against your identity provider. The file is yours to edit (generated-once).",
239
+ });
240
+ }
241
+ }
242
+ }
243
+
195
244
  export async function validateProject(projectRoot: string = process.cwd()): Promise<ProjectValidationResult> {
196
245
  const loaded = await loadProjectManifest(projectRoot);
197
246
  const rebuilt = await buildProjectManifest(projectRoot);
@@ -231,6 +280,7 @@ export async function validateProject(projectRoot: string = process.cwd()): Prom
231
280
 
232
281
  validateGeneratedArtifacts(loaded.manifest, violations);
233
282
  validateLayeringViolations(projectRoot, violations);
283
+ validateExternalTokenVerifierStubs(projectRoot, loaded.manifest, violations);
234
284
 
235
285
  if (loaded.source === "file") {
236
286
  validateManifestDrift(projectRoot, loaded.manifest, rebuilt.manifest, violations);
@@ -60,7 +60,9 @@ Goal: add a data model and derive CRUD/BFF/UI/infra deterministically.
60
60
  5. Verify:
61
61
  \`${runCmd} swallowkit machine verify project\`
62
62
  - \`summary.done: false\` → \`${runCmd} swallowkit machine explain failure\` and repair, then re-verify.
63
- 6. If \`dev-seeds/\` exists, update seed JSON for the new model.
63
+ 6. If \`dev-seeds/\` exists, update seed JSON for the new model. Seeds are only
64
+ applied by \`${runCmd} swallowkit dev --seed-env <environment>\` — editing the
65
+ JSON alone does nothing.
64
66
 
65
67
  ## Terminal states
66
68
 
@@ -103,6 +105,49 @@ and regenerate. Hand-edits are detected as drift and block unattended overwrites
103
105
  `,
104
106
  };
105
107
 
108
+ const addAuth: WorkflowDoc = {
109
+ fileName: "add-auth.md",
110
+ title: "Add authentication (plan/apply with config ownership rules)",
111
+ skillName: "swallowkit-add-auth",
112
+ skillDescription:
113
+ "Add authentication (custom-jwt, Azure Static Web Apps built-in, or external-token) to a SwallowKit project via plan/apply auth, including the config ownership rules (never hand-write auth.schemes; hand-edit policies and allowedProviders after apply) and post-apply steps. Use when asked to add login, authentication, authorization, roles, or a named auth scheme.",
114
+ content: `# Workflow: Add authentication
115
+
116
+ Goal: introduce an auth scheme deterministically and finish the manual follow-ups.
117
+
118
+ ## Config ownership (critical)
119
+
120
+ - \`auth.schemes\` is **owned by plan/apply auth. Never hand-write scheme entries** —
121
+ a pre-written scheme makes planning fail with \`already exists\`.
122
+ - \`auth.authorization.policies\` and \`swa.allowedProviders\` are **hand-edited after
123
+ apply** (the config is an extension point; this is the expected workflow).
124
+
125
+ ## Steps
126
+
127
+ 1. Check what already exists: \`${runCmd} swallowkit machine inspect capabilities\`
128
+ (authentication section) and the current \`auth\` block in swallowkit.config.
129
+ 2. Plan (writes nothing):
130
+ \`${runCmd} swallowkit machine plan auth --provider <custom-jwt|swa|external-token> [--scheme <name>] [--allowed-providers github,aad]\`
131
+ 3. Apply:
132
+ \`${runCmd} swallowkit machine apply auth --plan <planId>\`
133
+ 4. Follow the returned \`nextActions\`:
134
+ - Define \`auth.authorization.policies\` referencing the scheme (hand-edit).
135
+ - swa: confirm \`swa.allowedProviders\` — generated login URLs use the first entry.
136
+ - external-token: implement the generated verifier stub. It fails closed and
137
+ **verify passes with the stub**, so a green verify does not mean the auth
138
+ flow works (a \`auth-verifier-stub\` warning is surfaced while unmodified).
139
+ 5. Add \`authPolicy\` to models that need guards, then re-run plan/apply scaffold
140
+ for them.
141
+ 6. Verify: \`${runCmd} swallowkit machine verify project\`
142
+
143
+ ## Scope guardrail
144
+
145
+ Generated CRUD enforces authentication and roles but does **not** scope rows to
146
+ the caller. Owner-scoped behavior belongs in ai-authored endpoints that derive
147
+ the scoping key from the verified principal, never from the request body.
148
+ `,
149
+ };
150
+
106
151
  const verifyRepair: WorkflowDoc = {
107
152
  fileName: "verify-and-repair.md",
108
153
  title: "Verify and repair loop",
@@ -190,6 +235,7 @@ under \`.github/skills/\` so skills-aware agents load them automatically.
190
235
  |----------|------|-------|
191
236
  | Add a new model | [add-model.md](./add-model.md) | \`swallowkit-add-model\` |
192
237
  | Modify an existing model | [modify-model.md](./modify-model.md) | \`swallowkit-modify-model\` |
238
+ | Add authentication | [add-auth.md](./add-auth.md) | \`swallowkit-add-auth\` |
193
239
  | Verify and repair | [verify-and-repair.md](./verify-and-repair.md) | \`swallowkit-verify-repair\` |
194
240
  | Provision Azure resources | [provision.md](./provision.md) | \`swallowkit-provision\` |
195
241
 
@@ -203,7 +249,7 @@ Full machine-readable contract: \`${runCmd} swallowkit machine inspect boundarie
203
249
  `,
204
250
  };
205
251
 
206
- return [index, addModel, modifyModel, verifyRepair, provision];
252
+ return [index, addModel, modifyModel, addAuth, verifyRepair, provision];
207
253
  }
208
254
 
209
255
  export const WORKFLOWS_DIR = path.join(".swallowkit", "workflows");
@@ -61,6 +61,11 @@ function safeIdentifier(name: string): string {
61
61
  return name.replace(/[^A-Za-z0-9_]/g, "_");
62
62
  }
63
63
 
64
+ /** C# の string[] リテラル。空のときは型推論不能な `new[] { }` (CS0826) を避けて Array.Empty<string>() を出力する。 */
65
+ function csharpStringArray(items: string[]): string {
66
+ return items.length > 0 ? `new[] { ${items.map((item) => `"${item}"`).join(", ")} }` : "Array.Empty<string>()";
67
+ }
68
+
64
69
  function schemeSlug(name: string): string {
65
70
  return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
66
71
  }
@@ -123,10 +128,10 @@ public static class AuthRouter
123
128
  {
124
129
  private static readonly Dictionary<string, (string[] Schemes, string[] Roles)> Policies = new()
125
130
  {
126
- ${Object.entries(config.authorization.policies).map(([n,p]) => ` ["${n}"] = (new[] { ${p.schemes.map(s=>`"${s}"`).join(", ")} }, new[] { ${(p.roles??[]).map(r=>`"${r}"`).join(", ")} }),`).join("\n")}
131
+ ${Object.entries(config.authorization.policies).map(([n,p]) => ` ["${n}"] = (${csharpStringArray(p.schemes)}, ${csharpStringArray(p.roles??[])}),`).join("\n")}
127
132
  };
128
133
  private static readonly Dictionary<string,string> Providers = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]="${s.provider}"`).join(", ")} };
129
- private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]=new[] { ${(s.swa?.allowedProviders??[]).map(p=>`"${p}"`).join(", ")} }`).join(", ")} };
134
+ private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]=${csharpStringArray(s.swa?.allowedProviders??[])}`).join(", ")} };
130
135
  public static async Task<(AuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, string policyName)
131
136
  {
132
137
  if (!Policies.TryGetValue(policyName, out var policy)) return (null, await Error(request, HttpStatusCode.Unauthorized, "Undefined authorization policy"));
@@ -29,6 +29,8 @@ export interface VerifyCheckEvidence {
29
29
  violations?: ProjectViolation[];
30
30
  findings?: DriftFinding[];
31
31
  logTail?: string[];
32
+ /** --compact で抑制された info-severity findings の件数。 */
33
+ suppressedInfoFindings?: number;
32
34
  }
33
35
 
34
36
  export interface VerifyCheckResult {
@@ -213,6 +215,23 @@ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
213
215
  };
214
216
  }
215
217
 
218
+ // typecheck は shared のビルド済み dist/ を参照する。古い dist による
219
+ // 「no exported member」誤検知を防ぐため、先に shared をビルドする。
220
+ const sharedBuild = runSharedBuildIfPresent(projectRoot);
221
+ if (sharedBuild.ran && !sharedBuild.ok) {
222
+ return {
223
+ id: "typecheck",
224
+ title: "TypeScript type check",
225
+ status: "fail",
226
+ durationMs: Date.now() - startedAt,
227
+ fixable: true,
228
+ evidence: { command: sharedBuild.command, logTail: sharedBuild.logTail },
229
+ suggestedActions: [
230
+ "Fix the shared package build errors first; typecheck resolves the shared package from its built dist/.",
231
+ ],
232
+ };
233
+ }
234
+
216
235
  const scripts = readPackageScripts(projectRoot);
217
236
  let command: string;
218
237
  let args: string[];
@@ -240,6 +259,42 @@ const SCRIPT_CHECK_TITLES: Record<string, string> = {
240
259
  test: "Test suite",
241
260
  };
242
261
 
262
+ interface SharedBuildOutcome {
263
+ ran: boolean;
264
+ ok: boolean;
265
+ command?: string;
266
+ logTail?: string[];
267
+ }
268
+
269
+ /** shared/package.json に build script があれば実行する (なければ no-op)。 */
270
+ function runSharedBuildIfPresent(projectRoot: string): SharedBuildOutcome {
271
+ const sharedPackageJsonPath = path.join(projectRoot, "shared", "package.json");
272
+ if (!fs.existsSync(sharedPackageJsonPath)) return { ran: false, ok: true };
273
+ try {
274
+ const pkg = JSON.parse(fs.readFileSync(sharedPackageJsonPath, "utf-8")) as { scripts?: Record<string, string> };
275
+ if (typeof pkg.scripts?.build !== "string") return { ran: false, ok: true };
276
+ } catch {
277
+ return { ran: false, ok: true };
278
+ }
279
+
280
+ const pm = detectFromProject(projectRoot);
281
+ const command = getCommands(pm).name;
282
+ const result = spawnSync(command, ["run", "build"], {
283
+ cwd: path.join(projectRoot, "shared"),
284
+ shell: true,
285
+ encoding: "utf-8",
286
+ timeout: 5 * 60 * 1000,
287
+ });
288
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
289
+ const ok = result.status === 0 && !result.error;
290
+ return {
291
+ ran: true,
292
+ ok,
293
+ command: `${command} run build (in shared/)`,
294
+ logTail: ok ? undefined : tailLines(result.error ? `${output}\n${result.error.message}` : output, 50),
295
+ };
296
+ }
297
+
243
298
  function runScriptCheck(projectRoot: string, scriptName: "build" | "lint" | "test"): VerifyCheckResult {
244
299
  const startedAt = Date.now();
245
300
  const scripts = readPackageScripts(projectRoot);
@@ -358,6 +413,27 @@ export async function runVerify(
358
413
  return result;
359
414
  }
360
415
 
416
+ /**
417
+ * Agent 消費向けに info-severity の drift findings (「user-owned が編集されたが想定内」等)
418
+ * を除去したコンパクトな結果を返す。判定 (summary) は変わらない。
419
+ */
420
+ export function compactVerifyResult(result: VerifyResult): VerifyResult {
421
+ return {
422
+ ...result,
423
+ checks: result.checks.map((check) => {
424
+ const findings = check.evidence.findings;
425
+ if (!findings || findings.length === 0) return check;
426
+ const kept = findings.filter((finding) => finding.severity !== "info");
427
+ const suppressed = findings.length - kept.length;
428
+ if (suppressed === 0) return check;
429
+ return {
430
+ ...check,
431
+ evidence: { ...check.evidence, findings: kept, suppressedInfoFindings: suppressed },
432
+ };
433
+ }),
434
+ };
435
+ }
436
+
361
437
  export interface FailureExplanation {
362
438
  checkId: string;
363
439
  title: string;