@fabrika/builder 0.3.3 → 0.4.0
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/README.md +5 -3
- package/cli/src/commands/init.ts +6 -0
- package/cli/src/inject-skills.ts +22 -5
- package/daemon/src/platform-remote.ts +6 -1
- package/daemon/src/platform.ts +4 -1
- package/decisions/patterns/zerops-control-deploy.md +8 -0
- package/decisions/patterns/zerops-local-app.md +32 -14
- package/decisions/patterns/zerops-postgres-seeds.md +15 -5
- package/decisions/technologies/cloudflare-access.md +1 -1
- package/decisions/technologies/zerops-fabrika-auth.md +49 -30
- package/decisions/technologies/zerops-platform.md +45 -23
- package/package.json +8 -8
- package/shared/index.ts +3 -2
- package/skill/agents/implementer.md +2 -0
- package/stacks/zerops-app.md +19 -9
- package/ui/dist/assets/{client.krrpk10y.js → client.2027sv30.js} +14 -14
- package/ui/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -17,14 +17,15 @@ The current phase is derived from files in `.fabrika/`; it is not stored as a mu
|
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
19
|
|
|
20
|
-
Install the public CLI and initialize the builder in an
|
|
20
|
+
Install the public CLI with this package, which it loads but does not install, and initialize the builder in an
|
|
21
|
+
application repository:
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
bun add --global @fabrika/cli
|
|
24
|
+
bun add --global @fabrika/cli @fabrika/builder
|
|
24
25
|
fabrika builder init
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
Initialization installs the Fabrika skill for Claude Code and Codex, starts the local daemon, and opens the dashboard. The coding agent then drives the phase loop.
|
|
28
|
+
Initialization installs the Fabrika skill and the `fabrika-app` application rules (from `@fabrika/app`) for Claude Code and Codex, starts the local daemon, and opens the dashboard. The coding agent then drives the phase loop.
|
|
28
29
|
|
|
29
30
|
```text
|
|
30
31
|
fabrika builder next
|
|
@@ -70,6 +71,7 @@ Connected initialization uses `zerops-app` for a new project. A person approves
|
|
|
70
71
|
issues/
|
|
71
72
|
.claude/
|
|
72
73
|
.agents/skills/fabrika/
|
|
74
|
+
.agents/skills/fabrika-app/
|
|
73
75
|
.codex/agents/
|
|
74
76
|
tests/browser/*.test.ts
|
|
75
77
|
```
|
package/cli/src/commands/init.ts
CHANGED
|
@@ -74,6 +74,12 @@ export async function cmdInit(argv: string[]): Promise<number> {
|
|
|
74
74
|
+ `${support.written.length} installed, ${support.updated.length} updated, `
|
|
75
75
|
+ `${support.skipped.length} current, ${support.preserved.length} user-edited preserved`,
|
|
76
76
|
)
|
|
77
|
+
if (support.appSkill?.from === 'builder') {
|
|
78
|
+
console.error(
|
|
79
|
+
`[fabrika] the application has not installed @fabrika/app: installed the builder's fabrika-app rules `
|
|
80
|
+
+ `(${support.appSkill.version}); run bun install, then fabrika builder init again for the pinned version`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
77
83
|
} else {
|
|
78
84
|
console.error(`[fabrika] agent support: source dir not found, skipped installation`)
|
|
79
85
|
}
|
package/cli/src/inject-skills.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { APP_AGENT_SKILL_NAME, type AppAgentSkill, installedAppAgentSkill, readAppAgentSkill } from '@fabrika/app/agent-skill'
|
|
1
2
|
import { createHash } from 'crypto'
|
|
2
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'fs'
|
|
3
4
|
import { dirname, join, relative, resolve } from 'path'
|
|
@@ -21,9 +22,16 @@ function isAgentHost(value: string): value is AgentHost {
|
|
|
21
22
|
return value === 'claude' || value === 'codex'
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** Where the installed `fabrika-app` skill came from: the application's own `@fabrika/app`, or the builder's. */
|
|
26
|
+
export interface AppSkillSource {
|
|
27
|
+
from: 'application' | 'builder'
|
|
28
|
+
version: string
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
export interface InjectResult {
|
|
25
32
|
sourceDir: string | null
|
|
26
33
|
hosts: AgentHost[]
|
|
34
|
+
appSkill: AppSkillSource | null
|
|
27
35
|
written: string[]
|
|
28
36
|
updated: string[]
|
|
29
37
|
skipped: string[]
|
|
@@ -72,6 +80,7 @@ export function injectAgentSupport(repoRoot: string, opts: InjectOptions = {}):
|
|
|
72
80
|
const result: InjectResult = {
|
|
73
81
|
sourceDir,
|
|
74
82
|
hosts,
|
|
83
|
+
appSkill: null,
|
|
75
84
|
written: [],
|
|
76
85
|
updated: [],
|
|
77
86
|
skipped: [],
|
|
@@ -80,7 +89,11 @@ export function injectAgentSupport(repoRoot: string, opts: InjectOptions = {}):
|
|
|
80
89
|
if (!sourceDir) return result
|
|
81
90
|
|
|
82
91
|
const root = resolve(repoRoot)
|
|
83
|
-
|
|
92
|
+
// The application's pinned `@fabrika/app` is the version its code compiles against; before `bun install` there is none.
|
|
93
|
+
const pinned = installedAppAgentSkill(root)
|
|
94
|
+
const appSkill = pinned ?? readAppAgentSkill()
|
|
95
|
+
result.appSkill = { from: pinned === null ? 'builder' : 'application', version: appSkill.version }
|
|
96
|
+
const desired = renderDesiredFiles(sourceDir, hosts, appSkill)
|
|
84
97
|
const manifestPath = join(root, MANIFEST_RELATIVE_PATH)
|
|
85
98
|
const manifest = readManifest(manifestPath)
|
|
86
99
|
|
|
@@ -100,21 +113,25 @@ function normalizeHosts(hosts?: AgentHost[]): AgentHost[] {
|
|
|
100
113
|
return [...new Set(selected)].sort()
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
function renderDesiredFiles(sourceDir: string, hosts: AgentHost[]): DesiredFile[] {
|
|
116
|
+
function renderDesiredFiles(sourceDir: string, hosts: AgentHost[], appSkill: AppAgentSkill): DesiredFile[] {
|
|
104
117
|
const skill = readFileSync(join(sourceDir, 'SKILL.md'), 'utf-8')
|
|
105
118
|
const agents = readAgentSources(join(sourceDir, 'agents'))
|
|
106
119
|
const resources = readSkillResources(sourceDir)
|
|
107
120
|
const files: DesiredFile[] = []
|
|
108
121
|
|
|
109
122
|
for (const host of hosts) {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
: join('.agents', 'skills', 'fabrika')
|
|
123
|
+
const skillsDir = host === 'claude' ? join('.claude', 'skills') : join('.agents', 'skills')
|
|
124
|
+
const skillRoot = join(skillsDir, 'fabrika')
|
|
113
125
|
files.push({
|
|
114
126
|
relativePath: join(skillRoot, 'SKILL.md'),
|
|
115
127
|
content: host === 'claude' ? renderClaudeSkill(skill) : skill,
|
|
116
128
|
host,
|
|
117
129
|
})
|
|
130
|
+
const appSkillRoot = join(skillsDir, APP_AGENT_SKILL_NAME)
|
|
131
|
+
files.push({ relativePath: join(appSkillRoot, 'SKILL.md'), content: appSkill.text, host })
|
|
132
|
+
for (const reference of appSkill.references) {
|
|
133
|
+
files.push({ relativePath: join(appSkillRoot, reference.path), content: reference.text, host })
|
|
134
|
+
}
|
|
118
135
|
for (const resource of resources) {
|
|
119
136
|
if (host === 'claude' && resource.relativePath === join('agents', 'openai.yaml')) continue
|
|
120
137
|
files.push({
|
|
@@ -31,6 +31,11 @@ const environmentsSchema = z.object({
|
|
|
31
31
|
domain: storedTextSchema.nullable(),
|
|
32
32
|
publicOrigin: storedTextSchema.nullable(),
|
|
33
33
|
triggerRef: storedTextSchema.nullable(),
|
|
34
|
+
authoring: z.boolean(),
|
|
35
|
+
protected: z.boolean(),
|
|
36
|
+
publishedRunId: storedTextSchema.nullable(),
|
|
37
|
+
// A Control older than the field lists none.
|
|
38
|
+
deviations: z.array(storedTextSchema).default([]),
|
|
34
39
|
namespaceId: storedTextSchema.nullable(),
|
|
35
40
|
provider: storedTextSchema,
|
|
36
41
|
target: envelopeSchema,
|
|
@@ -44,7 +49,7 @@ const runSchema = z.object({
|
|
|
44
49
|
env: storedTextSchema,
|
|
45
50
|
ref: storedTextSchema,
|
|
46
51
|
commitSha: storedTextSchema.nullable(),
|
|
47
|
-
status: z.enum(['pending', 'running', 'succeeded', 'failed']),
|
|
52
|
+
status: z.enum(['pending', 'running', 'awaiting_approval', 'succeeded', 'failed']),
|
|
48
53
|
})
|
|
49
54
|
|
|
50
55
|
export async function boundedJson(response: Response): Promise<unknown> {
|
package/daemon/src/platform.ts
CHANGED
|
@@ -470,10 +470,13 @@ export class BuilderPlatform {
|
|
|
470
470
|
? 'Deployment succeeded. The configured URL has not been checked for reachability.'
|
|
471
471
|
: run.status === 'failed'
|
|
472
472
|
? 'Platform deployment failed. See the console for details.'
|
|
473
|
+
: run.status === 'awaiting_approval'
|
|
474
|
+
? 'The deployment changes access on a protected environment and waits for a person to approve it in the console.'
|
|
473
475
|
: 'Platform deployment is in progress.',
|
|
474
476
|
}
|
|
475
477
|
this.storage.saveProject(project)
|
|
476
|
-
|
|
478
|
+
// A parked run waits for a person, not for the platform; the builder key cannot approve it.
|
|
479
|
+
if (terminal || run.status === 'awaiting_approval') return
|
|
477
480
|
await this.sleep(3000, signal)
|
|
478
481
|
}
|
|
479
482
|
throw new ObservationError('Deployment polling timed out. Inspect the saved run in the platform console.')
|
|
@@ -13,6 +13,8 @@ sources:
|
|
|
13
13
|
read: "Current Control deployment contract and provider lifecycle; do not infer builder command syntax."
|
|
14
14
|
- where: "examples/zerops-app/README.md + fabrika.config.ts + zerops.yaml in fabrika-platform"
|
|
15
15
|
read: "Application-owned source artifacts versus platform-owned deployment and secrets."
|
|
16
|
+
- where: "docs/decisions/0050-approved-access.md in fabrika-platform"
|
|
17
|
+
read: "The access snapshot Control reads from each commit, and which changes wait for a person's approval."
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
# Deploy through installed Control
|
|
@@ -35,6 +37,12 @@ prerequisites are a handoff to that administrator.
|
|
|
35
37
|
- Control and the Zerops provider supply credentials server-side, deliver source
|
|
36
38
|
and trigger Zerops' build/deploy pipeline. The local coding agent does not need
|
|
37
39
|
a Zerops account token or an installation provisioning credential.
|
|
40
|
+
- Control reads the contract, `src/db/tables.ts` and the migration list from the
|
|
41
|
+
deployed commit and publishes only that approved snapshot to the proxy and IAM.
|
|
42
|
+
A widening (a new endpoint, caller, role grant, table policy change or migration)
|
|
43
|
+
on a protected environment (`prod` by default) parks the run in
|
|
44
|
+
`awaiting_approval` until a person approves it in the console. The agent cannot
|
|
45
|
+
approve; it reports the parked run and what the change lets someone do.
|
|
38
46
|
- Follow the Control run to its terminal result and verify its requested revision
|
|
39
47
|
and public proxy URL. A successful build, a trigger response or a local emulator
|
|
40
48
|
result alone is not proof of a successful production release.
|
|
@@ -9,8 +9,10 @@ recommends: [tailwind, opice]
|
|
|
9
9
|
conflicts: [single-worker-spa]
|
|
10
10
|
status: wip
|
|
11
11
|
sources:
|
|
12
|
-
- where: "examples/zerops-app/src/
|
|
13
|
-
read: "
|
|
12
|
+
- where: "examples/zerops-app/fabrika.contract.ts + src/notes/endpoints.ts + src/app.ts + README.md in fabrika-platform"
|
|
13
|
+
read: "The contract's mounts, `serve` over the data layer, the SPA build, and the limits of the example's local run instructions."
|
|
14
|
+
- where: "docs/reference/application-runtime.md (Serving a contract, Testing a contract application) in fabrika-platform"
|
|
15
|
+
read: "The generated entrypoint, the framework variables `boot` needs, `testKit` and the generated tests."
|
|
14
16
|
- where: "docs/reference/local-development.md + packages/local-stack/CLAUDE.md in fabrika-platform"
|
|
15
17
|
read: "Real IAM/proxy/Postgres topology, local endpoints and IAM-owned login bypass."
|
|
16
18
|
- where: "packages/local-stack/src/app-registration.ts in fabrika-platform"
|
|
@@ -19,16 +21,28 @@ sources:
|
|
|
19
21
|
|
|
20
22
|
# Local Bun SPA and API
|
|
21
23
|
|
|
22
|
-
Apply this to a client-rendered React/Buzola app served with its API by
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
Apply this to a client-rendered React/Buzola app served with its API by the
|
|
25
|
+
generated Bun entrypoint of a contract application. The contract declares both:
|
|
26
|
+
the SPA as `c.spa(prefix, { dir, access })`, served from the built asset
|
|
27
|
+
directory for signed-in people, and the API as a `c.rpc` mount. An unknown
|
|
28
|
+
procedure under an RPC prefix is 404, never the SPA. Use Bun-native asset bundling
|
|
29
|
+
and Buzola codegen; the SPA's build commands are the only commands the authored
|
|
30
|
+
`zerops.yaml` may add. The SPA reads no `import.meta.env` other than
|
|
31
|
+
`FABRIKA_PUBLIC_*` and calls the API through `createClient(contract, 'api')` of
|
|
32
|
+
`@fabrika/app/client`.
|
|
25
33
|
|
|
26
34
|
## Local composition to scaffold
|
|
27
35
|
|
|
28
36
|
The development loop needs PostgreSQL, real IAM, a proxy with this application's
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
endpoint table (`contractProxyTable` of the compiled contract) and a private Bun
|
|
38
|
+
upstream started the way the platform starts it: `fabrika db migrate`, then
|
|
39
|
+
`fabrika/entry.generated.ts` (`fabrika app build` writes it) with the framework
|
|
40
|
+
variables `boot` refuses to start without: `FABRIKA_APP_ID`,
|
|
41
|
+
`FABRIKA_APP_DATABASE_URL`, `FABRIKA_IAM_ISSUER`, `FABRIKA_IAM_APP_ID`,
|
|
42
|
+
`FABRIKA_CONTRACT_DIGEST`, a `FABRIKA_PROXY_SECRET` of at least 32 characters that
|
|
43
|
+
the proxy sends, and, for a `c.files` collection, `FABRIKA_APP_STORAGE_*`.
|
|
44
|
+
Register the application's `schemaFromContract` schema and exact local proxy
|
|
45
|
+
return origin with local IAM. Keep the local database and credentials separate
|
|
32
46
|
from the installed production platform.
|
|
33
47
|
|
|
34
48
|
Use the existing Fabrika local-stack composition as the reference. From the
|
|
@@ -37,12 +51,16 @@ services, and `bun run local:status` reports them. It pre-registers the fixture
|
|
|
37
51
|
apps, not every new builder project. The public notes fixture is available at
|
|
38
52
|
`http://notes.fabrika.localhost:18081` through its proxy.
|
|
39
53
|
|
|
40
|
-
A new app needs its own documented local service wiring and registration
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
A new app needs its own documented local service wiring and registration. Establish
|
|
55
|
+
that wiring as the app's foundation work and verify it before reporting a working
|
|
56
|
+
preview. If the local runtime cannot reach IAM, or a required registration cannot
|
|
57
|
+
be made, report the missing prerequisite rather than adding fake auth or bypassing
|
|
58
|
+
the proxy.
|
|
59
|
+
|
|
60
|
+
Before the browser loop, run the application's own checks: `bun test` with
|
|
61
|
+
`FABRIKA_TEST_POSTGRES_URL` set (a database test that skips proves nothing),
|
|
62
|
+
`fabrika db test-scaffold` after every contract or table change, and
|
|
63
|
+
`fabrika-check`.
|
|
46
64
|
|
|
47
65
|
## Preview and acceptance
|
|
48
66
|
|
|
@@ -11,16 +11,26 @@ status: stable
|
|
|
11
11
|
sources:
|
|
12
12
|
- where: "packages/builder/decisions/technologies/opice.md in fabrika-platform"
|
|
13
13
|
read: "Scenario seeds metadata and its filename contract."
|
|
14
|
-
- where: "examples/zerops-app/
|
|
15
|
-
read: "
|
|
14
|
+
- where: "examples/zerops-app/db/migrations/ + src/db/tables.ts in fabrika-platform"
|
|
15
|
+
read: "The application schema, its scope columns, and the table policies a seeded row must satisfy."
|
|
16
|
+
- where: "packages/cli/CLAUDE.md (the `db` area) in fabrika-platform"
|
|
17
|
+
read: "`fabrika db migrate` and its environment: `FABRIKA_APP_ID`, `FABRIKA_APP_DATABASE_URL`, schema `app_<id>`."
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
# Named PostgreSQL fixtures
|
|
19
21
|
|
|
20
22
|
Apply this to the preset's local PostgreSQL database. An opice scenario naming
|
|
21
|
-
`seeds: ['example']` requires `seeds/example.sql`. Run
|
|
22
|
-
the named SQL files
|
|
23
|
-
the scenario. Document that local seed
|
|
23
|
+
`seeds: ['example']` requires `seeds/example.sql`. Run `fabrika db migrate` first,
|
|
24
|
+
then apply the named SQL files with `psql` against the application's schema
|
|
25
|
+
`app_<id>` (`SET search_path`) before the scenario. Document that local seed
|
|
26
|
+
command in the scaffold. Seeds are a local test tool, not application code: the
|
|
27
|
+
application reaches rows only through its scoped stores, and the checker refuses
|
|
28
|
+
raw SQL and database drivers in it.
|
|
29
|
+
|
|
30
|
+
A seeded row must look like one the data layer wrote: it carries its whole
|
|
31
|
+
containment chain (every scope column of its table policy) and its `createdBy` and
|
|
32
|
+
`version` columns where the policy names them, so the access forms find it in the
|
|
33
|
+
scope the scenario's principal is granted.
|
|
24
34
|
|
|
25
35
|
Use stable fixture IDs and PostgreSQL `INSERT ... ON CONFLICT` semantics so
|
|
26
36
|
repeated application is safe. Tests that mutate fixtures must restore their
|
|
@@ -46,5 +46,5 @@ adopted native Propustka sessions.
|
|
|
46
46
|
## Relationships — why
|
|
47
47
|
|
|
48
48
|
- **conflicts `propustka`** — current Propustka owns native SSO sessions and
|
|
49
|
-
|
|
49
|
+
request authorization; it no longer expects a Cloudflare Access edge.
|
|
50
50
|
- **conflicts `auth-password-kv`** — both fill the `authentication` slot. Access = managed SSO; auth-password-kv = app-owned password+session. Pick one.
|
|
@@ -1,53 +1,72 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: zerops-fabrika-auth
|
|
3
3
|
kind: technology
|
|
4
|
-
title: Fabrika IAM with
|
|
4
|
+
title: Fabrika IAM with contract enforcement at the proxy on Zerops
|
|
5
5
|
slot: authz
|
|
6
6
|
provides: [authentication, authorization, proxy-enforcement]
|
|
7
7
|
requires: [zerops-platform]
|
|
8
8
|
recommends: []
|
|
9
9
|
conflicts: [propustka, cloudflare-access, auth-password-kv]
|
|
10
|
-
npm: ["@fabrika/
|
|
10
|
+
npm: ["@fabrika/app@^0.0.4", "@fabrika/db@^0.0.4", "zod@^3.25.76"]
|
|
11
11
|
status: wip
|
|
12
12
|
sources:
|
|
13
|
-
- where: "packages/
|
|
14
|
-
read: "
|
|
15
|
-
- where: "examples/zerops-app/
|
|
16
|
-
read: "
|
|
13
|
+
- where: "packages/app/skill/SKILL.md in fabrika-platform (the `fabrika-app` skill)"
|
|
14
|
+
read: "The application rules: contract first, access forms, table policies, what the checker refuses, what is not built."
|
|
15
|
+
- where: "examples/zerops-app/src/vocabulary.ts + src/db/tables.ts + src/notes/endpoints.ts + fabrika.contract.ts in fabrika-platform"
|
|
16
|
+
read: "A worked contract: the vocabulary, the `/api` mount, the access forms and the handlers beside them."
|
|
17
|
+
- where: "docs/reference/application-runtime.md (Contracts, Serving a contract) in fabrika-platform"
|
|
18
|
+
read: "What the proxy and `boot` check per request, in order, and what `boot` refuses to start with."
|
|
17
19
|
- where: "docs/decisions/0022-the-proxy-is-the-only-enforcement-point.md + 0023-one-session-per-host.md in fabrika-platform"
|
|
18
|
-
read: "
|
|
20
|
+
read: "One evaluator at the proxy, the endpoint table, per-host session handoff."
|
|
19
21
|
---
|
|
20
22
|
|
|
21
|
-
# Fabrika IAM with
|
|
23
|
+
# Fabrika IAM with contract enforcement at the proxy
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
The application declares its whole access surface in `fabrika.contract.ts`:
|
|
26
|
+
`defineContract(vocabulary, (c) => surface)` from `@fabrika/app/contract`. The
|
|
27
|
+
vocabulary is its dimensions (e.g. `workspace` → `note`), exact action names and
|
|
28
|
+
roles that list actions (no `*`). Every endpoint is a procedure under a mount
|
|
29
|
+
(`c.rpc('/api', { callers: ['human', 'machine'] }, …)`) with an access form, plus
|
|
30
|
+
`c.health('/healthz')`, which Zerops requires. There are no other routes,
|
|
31
|
+
middleware, `authenticate` or custom server.
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
`fabrika.config.ts` compiles it with `compileContract(contract, tables)` and hands
|
|
34
|
+
the provider the artifact (`target.contract`), the endpoint table
|
|
35
|
+
(`target.proxy: { upstream, ...contractProxyTable(artifact) }`) and the IAM
|
|
36
|
+
schema (`schema: schemaFromContract(artifact)`). The proxy reads only that
|
|
37
|
+
endpoint table; its manifest parser refuses any other form. Control reads the
|
|
38
|
+
contract from the deployed commit, and a widening on a protected environment
|
|
39
|
+
(`prod` by default) waits for a person to approve it in the console.
|
|
32
40
|
|
|
33
|
-
The
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
trusts an unverified header or gives the app a session-to-token exchange.
|
|
41
|
+
The proxy alone selects the endpoint, handles login and the reserved
|
|
42
|
+
`/__fabrika/auth/callback` route, checks the caller kind and the requirement, and
|
|
43
|
+
injects `X-Fabrika-Token` and `X-Fabrika-Endpoint`. App services stay private.
|
|
44
|
+
Never implement a second enforcement decision in the app, the frontend or a Caddy
|
|
45
|
+
route.
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
The framework, not application code, verifies the injected token (signature,
|
|
48
|
+
issuer, audience `<app>@<env>`, expiry), checks the proxy secret and the endpoint
|
|
49
|
+
header, then the caller kind, the requirement, admission and the strict input.
|
|
50
|
+
Per-object authorization is the access form: `on` (one object, 404 when missing
|
|
51
|
+
or not the caller's), `over` (a list, `ctx.within`), `in` (creation inside a
|
|
52
|
+
scope, 403 when not held). Handlers reach rows only through the stores of
|
|
53
|
+
`src/db/tables.ts` with the `Ref` or `Within` they are handed; they never take an
|
|
54
|
+
owner, user or tenant id from the input and never read `process.env`.
|
|
55
|
+
|
|
56
|
+
On Zerops an endpoint with `creates`, `ctx.members` or `ctx.links` reaches IAM
|
|
57
|
+
through the membership transport the platform writes (`FABRIKA_IAM_RPC_URL`,
|
|
58
|
+
`FABRIKA_IAM_RPC_KEY`). The key only pins the application; the caller's token
|
|
59
|
+
carries the authority, with the proxy's delegation grant for scope creation and
|
|
60
|
+
membership changes.
|
|
45
61
|
|
|
46
62
|
## Local authentication is the same model
|
|
47
63
|
|
|
48
64
|
Run real IAM and the proxy locally. IAM's own `LOCAL_DEV_LOGIN` mechanism can
|
|
49
65
|
create a real local session; it is not an application dev flag. Restricted-role
|
|
50
66
|
tests need real IAM principals and grants, not the bootstrap admin for every
|
|
51
|
-
scenario.
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
scenario. In unit tests, use `testKit` of `@fabrika/app/testing`, which runs the
|
|
68
|
+
production proxy decision and `boot` with a stand-in IAM; never synthesize a
|
|
69
|
+
caller in application code. When a request answers 401 or 403, report the missing
|
|
70
|
+
action or role; never loosen the contract, a role or a table policy to make it
|
|
71
|
+
pass. Credentials and raw request headers must not enter logs or committed
|
|
72
|
+
fixtures.
|
|
@@ -7,11 +7,13 @@ provides: [platform, private-services, managed-deploy]
|
|
|
7
7
|
requires: [bun, postgres]
|
|
8
8
|
recommends: []
|
|
9
9
|
conflicts: [cloudflare]
|
|
10
|
-
npm: ["@fabrika/provider-zerops@^0.0.4", "@fabrika/app@^0.0.4"]
|
|
10
|
+
npm: ["@fabrika/provider-zerops@^0.0.4", "@fabrika/app@^0.0.4", "@fabrika/cli@^0.0.4"]
|
|
11
11
|
status: wip
|
|
12
12
|
sources:
|
|
13
13
|
- where: "examples/zerops-app/fabrika.config.ts + zerops.yaml in fabrika-platform"
|
|
14
|
-
read: "Current defineApp shape
|
|
14
|
+
read: "Current defineApp shape with `target.contract`, the private runtime and the locked repository-root descriptor."
|
|
15
|
+
- where: "packages/provider-zerops/src/execution.ts + CLAUDE.md in fabrika-platform"
|
|
16
|
+
read: "The generated build, start script, readiness check and crontab of a contract application."
|
|
15
17
|
- where: "packages/provider-zerops/src/types.ts + src/index.ts in fabrika-platform"
|
|
16
18
|
read: "Provider-owned authoring contract and exports; verify installed versions before use."
|
|
17
19
|
- where: "docs/reference/zerops-platform.md in fabrika-platform"
|
|
@@ -25,40 +27,60 @@ Use this for applications deployed by an existing Fabrika installation. Import
|
|
|
25
27
|
`@fabrika/provider-zerops`. Keep `fabrika.config.ts` as the app's deploy source of
|
|
26
28
|
truth: stable `id`, `schema`, `pipeline` and `target` with `platform: 'zerops'`.
|
|
27
29
|
|
|
28
|
-
##
|
|
30
|
+
## Deploy configuration
|
|
29
31
|
|
|
30
32
|
- Declare services through `target.services(ctx)`. Select the runtime explicitly
|
|
31
33
|
with `target.deployService` when declaring multiple services. Set
|
|
32
34
|
`target.zeropsSetup` to the matching setup in repository-root `zerops.yaml`.
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
- A contract application hands `target.contract` (the compiled
|
|
36
|
+
`fabrika.contract.ts`) and `target.proxy: { upstream, ...contractProxyTable(artifact) }`
|
|
37
|
+
with the private runtime address (port 3000). Set runtime
|
|
38
|
+
`enableSubdomainAccess: false`; only the namespace proxy has public routing. A
|
|
39
|
+
custom domain belongs on the proxy, never the app service.
|
|
36
40
|
- Leave project placement to Control's environment/namespace registry. Keep
|
|
37
41
|
provider tokens and concrete project coordinates out of application source.
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`run
|
|
42
|
+
- The platform owns the setup of a contract application (ADR-0043). The authored
|
|
43
|
+
`zerops.yaml` keeps only `setup`, `build.base`, `build.os` and the SPA's
|
|
44
|
+
`build.buildCommands`; every other key is refused. The platform generates the
|
|
45
|
+
rest: a frozen install, the application's `bun test`, a reinstall, the
|
|
46
|
+
generated entrypoint, `fabrika-check`, `tsc`, the deploy gate on the contract's
|
|
47
|
+
health path, and the `run` section. Its start script runs `fabrika db migrate`,
|
|
48
|
+
`fabrika db codegen --verify` and `fabrika db test-isolation`, then
|
|
49
|
+
`fabrika/entry.generated.ts`; each `c.cron` job becomes a crontab entry
|
|
50
|
+
`fabrika job run <name>`. The build has no database, so the database tests skip
|
|
51
|
+
there and run at container start.
|
|
52
|
+
- Commit the Bun lockfile at the app repository root, and commit what
|
|
53
|
+
`fabrika db codegen` and `fabrika db test-scaffold` write. Never edit a
|
|
54
|
+
generated file (`fabrika/entry.generated.ts`, `src/db/schema.generated.ts`, the
|
|
55
|
+
generated tests) by hand; regenerate it with its command.
|
|
47
56
|
|
|
48
57
|
## Data and configuration
|
|
49
58
|
|
|
50
59
|
The default public config declares an app-owned PostgreSQL service. A cheap
|
|
51
60
|
namespace can instead supply the typed shared PostgreSQL binding: consult
|
|
52
|
-
`examples/zerops-app/fabrika.cheap.config.ts`
|
|
53
|
-
|
|
61
|
+
`examples/zerops-app/fabrika.cheap.config.ts` before selecting that variant.
|
|
62
|
+
Preserve the administrator's namespace assignment. A contract application has
|
|
63
|
+
one PostgreSQL database: the platform writes `FABRIKA_APP_DATABASE_URL` (the TLS
|
|
64
|
+
reference with the database name and `sslmode=require`) and `FABRIKA_APP_ID`, and
|
|
65
|
+
the application's tables live in the schema `app_<id>`. The application names no
|
|
66
|
+
connection string; `@fabrika/app/bun` opens the data layer for it.
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
must satisfy `^[a-z0-9]{1,25}$` and references may address only owned services and
|
|
58
|
-
declared namespace resources.
|
|
68
|
+
Service names must satisfy `^[a-z0-9]{1,25}$` and references may address only
|
|
69
|
+
owned services and declared namespace resources.
|
|
59
70
|
|
|
60
71
|
The provider owns isolation fields. Secret values live at service level in Zerops,
|
|
61
72
|
not in config, YAML or project-level environment variables. `pipeline.secrets`
|
|
62
73
|
names requirements; it does not add a deploy-time secret synchronization step.
|
|
63
|
-
Control supplies reserved
|
|
64
|
-
`FABRIKA_OPERATIONS_DSN
|
|
74
|
+
Control supplies the reserved `FABRIKA_*` variables (`FABRIKA_IAM_ISSUER`,
|
|
75
|
+
`FABRIKA_IAM_APP_ID`, `FABRIKA_OPERATIONS_DSN`, `FABRIKA_RELEASE`,
|
|
76
|
+
`FABRIKA_CONTRACT_DIGEST`, `FABRIKA_PROXY_SECRET`, …); do not redeclare them. The
|
|
77
|
+
framework takes every `FABRIKA_*` name out of the process before the application
|
|
78
|
+
loads; handlers get their own variables through `deps`.
|
|
79
|
+
|
|
80
|
+
A `c.files` collection needs one `object-storage` service in `fabrika.config.ts`;
|
|
81
|
+
the provider writes `FABRIKA_APP_STORAGE_*` from it as references Zerops resolves,
|
|
82
|
+
and refuses a second bucket.
|
|
83
|
+
|
|
84
|
+
An endpoint with `creates`, `ctx.members` or `ctx.links` reaches IAM through the
|
|
85
|
+
membership transport: the platform writes `FABRIKA_IAM_RPC_URL` and the
|
|
86
|
+
environment's own `FABRIKA_IAM_RPC_KEY`, and refuses an app config that sets them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local-first guided application design and agent-assisted implementation.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"zod": "^3.25.76",
|
|
50
|
-
"@fabrika/annotations": "^0.
|
|
51
|
-
"@fabrika/app": "^0.
|
|
52
|
-
"@fabrika/auth-core": "^0.
|
|
53
|
-
"@fabrika/canvas-kernel": "^0.
|
|
54
|
-
"@fabrika/control-contract": "^0.
|
|
55
|
-
"@fabrika/daemon-kit": "^0.
|
|
56
|
-
"@fabrika/provider-zerops": "^0.
|
|
50
|
+
"@fabrika/annotations": "^0.4.0",
|
|
51
|
+
"@fabrika/app": "^0.4.0",
|
|
52
|
+
"@fabrika/auth-core": "^0.4.0",
|
|
53
|
+
"@fabrika/canvas-kernel": "^0.4.0",
|
|
54
|
+
"@fabrika/control-contract": "^0.4.0",
|
|
55
|
+
"@fabrika/daemon-kit": "^0.4.0",
|
|
56
|
+
"@fabrika/provider-zerops": "^0.4.0",
|
|
57
57
|
"preact": "^10.25.0"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/shared/index.ts
CHANGED
|
@@ -30,7 +30,8 @@ export type BuilderPairingState =
|
|
|
30
30
|
| { status: 'connected' }
|
|
31
31
|
|
|
32
32
|
export interface BuilderDeploymentState {
|
|
33
|
-
|
|
33
|
+
/** `awaiting_approval`: the deploy widens access on a protected environment and waits for a person (ADR-0050). */
|
|
34
|
+
status: 'preparing' | 'pending' | 'running' | 'awaiting_approval' | 'succeeded' | 'failed' | 'cancelled'
|
|
34
35
|
runId: string | null
|
|
35
36
|
commitSha: string | null
|
|
36
37
|
message: string | null
|
|
@@ -290,7 +291,7 @@ export const builderPairingStateSchema = z.discriminatedUnion('status', [
|
|
|
290
291
|
z.object({ status: z.literal('connected') }).strict(),
|
|
291
292
|
])
|
|
292
293
|
export const builderDeploymentStateSchema = z.object({
|
|
293
|
-
status: z.enum(['preparing', 'pending', 'running', 'succeeded', 'failed', 'cancelled']),
|
|
294
|
+
status: z.enum(['preparing', 'pending', 'running', 'awaiting_approval', 'succeeded', 'failed', 'cancelled']),
|
|
294
295
|
runId: nonemptyStringSchema.nullable(),
|
|
295
296
|
commitSha: nonemptyStringSchema.nullable(),
|
|
296
297
|
message: nonemptyStringSchema.nullable(),
|
|
@@ -18,6 +18,8 @@ hints. Do not coordinate other issues and do not spawn more agents.
|
|
|
18
18
|
1. Read the complete issue body, its dependency outputs, relevant
|
|
19
19
|
`tests/browser/*.test.ts`, `.fabrika/architecture/tech.md`, and the absolute
|
|
20
20
|
stack/decision sources supplied by the parent.
|
|
21
|
+
When the app has a `fabrika.contract.ts`, apply the `fabrika-app` skill and
|
|
22
|
+
follow its rules.
|
|
21
23
|
2. Implement the smallest complete vertical slice required by `goal` and
|
|
22
24
|
`satisfies`: route, data operation, UI, migration, and seed where applicable.
|
|
23
25
|
3. Start the app using the selected stack's current run recipe. Apply the
|
package/stacks/zerops-app.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: zerops-app
|
|
3
3
|
title: "Platform-managed Zerops app — Bun + PostgreSQL + Fabrika IAM"
|
|
4
|
-
goal: "Build and verify a local React/Buzola
|
|
4
|
+
goal: "Build and verify a local React/Buzola contract application behind real IAM and the proxy's endpoint table, then deploy its available Git revision through installed Fabrika Control."
|
|
5
5
|
decisions:
|
|
6
6
|
- zerops-platform
|
|
7
7
|
- bun
|
|
@@ -31,8 +31,10 @@ Read installed contracts and the linked public examples before scaffolding.
|
|
|
31
31
|
## Composition
|
|
32
32
|
|
|
33
33
|
- `zerops-platform` owns provider configuration and private runtime services.
|
|
34
|
-
- `zerops-fabrika-auth` supplies
|
|
35
|
-
|
|
34
|
+
- `zerops-fabrika-auth` supplies the contract (`fabrika.contract.ts`), which the
|
|
35
|
+
proxy enforces as an endpoint table and the framework serves through `serve`.
|
|
36
|
+
- `zerops-local-app` composes the generated Bun entrypoint, the SPA mount and the
|
|
37
|
+
local IAM/proxy loop.
|
|
36
38
|
- `zerops-control-deploy` defines the human/admin/agent deployment boundary.
|
|
37
39
|
- `zerops-postgres-seeds` preserves opice's named SQL fixture contract on PostgreSQL.
|
|
38
40
|
|
|
@@ -52,15 +54,23 @@ or choose a replacement destination when prerequisites are missing.
|
|
|
52
54
|
## Local verification
|
|
53
55
|
|
|
54
56
|
1. Install with Bun and typecheck the actual provider config and application.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
After any change to the contract or `src/db/tables.ts`, run `fabrika db migrate`,
|
|
58
|
+
`fabrika db codegen` and `fabrika db test-scaffold`, and commit what they write.
|
|
59
|
+
2. Start the app's documented local composition: PostgreSQL, IAM, proxy and the
|
|
60
|
+
generated Bun entrypoint. Apply migrations and the scenario's PostgreSQL seeds
|
|
61
|
+
locally.
|
|
57
62
|
3. Open the app's **proxy URL**, complete IAM login, and run the targeted opice
|
|
58
63
|
scenario against that URL. Verify denied access with a real restricted identity.
|
|
59
|
-
4. Run application
|
|
60
|
-
|
|
64
|
+
4. Run application tests with `FABRIKA_TEST_POSTGRES_URL` set, `fabrika-check`,
|
|
65
|
+
typechecking and the production asset build. The SPA's build commands go in
|
|
66
|
+
`build.buildCommands` of the root `zerops.yaml`; the platform generates every
|
|
67
|
+
other key and deploys the checked build.
|
|
61
68
|
5. Report the actual local preview URL and checks. Production deployment follows
|
|
62
69
|
`zerops-control-deploy`; a passing local test is not a successful remote run.
|
|
63
70
|
|
|
64
|
-
The public notes example
|
|
65
|
-
|
|
71
|
+
The public notes example (`src/vocabulary.ts`, `src/db/tables.ts`, the `src/notes/` slice,
|
|
72
|
+
`fabrika.contract.ts`, `db/migrations/`, `src/app.ts` and its generated tests, plus an
|
|
73
|
+
`attachments` file collection, a `purge` job and a minimal SPA) supplies the backend/config
|
|
74
|
+
reference, not an arbitrary-project dev launcher. Its `AGENTS.md` carries the
|
|
75
|
+
`fabrika-app` rules. Scaffold and verify the local composition
|
|
66
76
|
described in `zerops-local-app` before claiming the application runs locally.
|