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.
- package/dist/cli/commands/add-auth.d.ts +3 -1
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +55 -21
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/auth-operations.d.ts +4 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -1
- package/dist/core/operations/auth-operations.js +5 -2
- package/dist/core/operations/auth-operations.js.map +1 -1
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +6 -2
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +58 -0
- package/dist/core/project/capabilities.d.ts.map +1 -0
- package/dist/core/project/capabilities.js +135 -0
- package/dist/core/project/capabilities.js.map +1 -0
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +48 -0
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +46 -2
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +6 -2
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +7 -0
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +69 -0
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +117 -41
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +32 -10
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/agent-loop-phase34.test.ts +66 -2
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/machine.test.ts +15 -0
- package/src/__tests__/multi-auth.test.ts +12 -0
- package/src/__tests__/plan-apply.test.ts +38 -0
- package/src/cli/commands/add-auth.ts +59 -21
- package/src/cli/index.ts +2 -0
- package/src/core/operations/auth-operations.ts +9 -2
- package/src/core/operations/create-model.ts +7 -2
- package/src/core/project/agent-templates/AGENTS.md +62 -2
- package/src/core/project/agent-templates/CLAUDE.md +3 -0
- package/src/core/project/agent-templates/copilot-instructions.md +3 -0
- package/src/core/project/agent-templates/shared-models.instructions.md +39 -2
- package/src/core/project/capabilities.ts +193 -0
- package/src/core/project/validation.ts +50 -0
- package/src/core/project/workflows.ts +48 -2
- package/src/core/scaffold/auth-generator.ts +7 -2
- package/src/core/verify/index.ts +76 -0
- package/src/machine/index.ts +133 -45
- 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
|
-
-
|
|
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
|
-
-
|
|
17
|
-
|
|
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}"] = (
|
|
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}"]
|
|
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"));
|
package/src/core/verify/index.ts
CHANGED
|
@@ -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;
|