@fungi.computer/bonsai 0.1.0 → 0.2.1
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/CHANGELOG.md +73 -0
- package/README.md +66 -12
- package/backend/build-workflow.ts +137 -0
- package/backend/index.ts +13 -7
- package/backend/methods.ts +209 -19
- package/package.json +54 -18
- package/scripts/release.mjs +12 -1
- package/scripts/shed-terminal-proof.mjs +10 -8
- package/shared/contract.ts +41 -0
- package/test/apps-build-install.test.ts +399 -26
- package/test/browser/build-workflow.browser.mjs +104 -0
- package/test/browser/repository-browser.fixture.mjs +84 -0
- package/test/browser/repository-settings-fixture.tsx +198 -2
- package/test/browser/repository-settings.browser.mjs +188 -26
- package/test/checks-workflow-template.test.mjs +12 -10
- package/test/favicon.test.mjs +25 -0
- package/test/git-push.native.mjs +30 -0
- package/test/git-push.worker.ts +51 -3
- package/test/merge-template-build.test.mjs +44 -201
- package/test/storyboard-save-https-git.fixture.mjs +35 -29
- package/test/storyboard-save-template.test.mjs +61 -12
- package/vite.config.ts +19 -1
- package/web/app-build-panel.tsx +183 -95
- package/web/bonsai.css +1 -0
- package/web/index.html +0 -1
- package/web/repo-settings.tsx +1 -1
- package/web/public/favicon.svg +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# @fungi.computer/bonsai
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @fungi.computer/oyster@0.3.1
|
|
9
|
+
|
|
10
|
+
## 0.2.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- Changes since 0.1.0:
|
|
15
|
+
|
|
16
|
+
- fix(oyster): keep Stage out of the starter release
|
|
17
|
+
- fix(shed): compact the model-facing command reply
|
|
18
|
+
- refactor(shed): send Workflow Git commands without any credential
|
|
19
|
+
- feat: show the Fungi sigil in the Bonsai and Tourtois tab
|
|
20
|
+
- fix(bonsai,oyster)!: share the repository App source wire through
|
|
21
|
+
workflows-wire
|
|
22
|
+
- fix(oyster,bonsai): export the types public signatures use
|
|
23
|
+
- refactor(bonsai): share authoring browser fixtures
|
|
24
|
+
- test(bonsai): follow Workflow-only Settings authoring
|
|
25
|
+
- fix(authoring): preserve Workflow refusals across lifecycle and native RPC
|
|
26
|
+
- Revert "fix(bonsai): remove impossible local App release command"
|
|
27
|
+
- fix(bonsai): remove impossible local App release command
|
|
28
|
+
- chore(b3a): checkpoint native workflow consumers and shared renderer types
|
|
29
|
+
- fix(b3a): align template and native proof consumers
|
|
30
|
+
- chore(b3a): checkpoint manual workflow integration
|
|
31
|
+
- refactor: move repository installation into Workflow nodes
|
|
32
|
+
- feat(apps): discover repository builds and install local Releases
|
|
33
|
+
- feat(apps): retain typed build installation outcomes in Settings
|
|
34
|
+
- test(apps): prove honest build refusals through host and Settings
|
|
35
|
+
- feat(bonsai): add repository App registration method and settings action
|
|
36
|
+
- fix(bonsai): clean up HTTPS Git fixture certificates
|
|
37
|
+
- feat(oyster): derive App build recipes from package metadata
|
|
38
|
+
- refactor(oyster)!: move the shell client into @fungi.computer/nacre
|
|
39
|
+
- refactor(computer)!: move the operation contract into computer-contract
|
|
40
|
+
- feat(oyster): add stage to the starter apps
|
|
41
|
+
- feat(oyster): public release 0.2.0
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- Updated dependencies
|
|
46
|
+
- Updated dependencies
|
|
47
|
+
- Updated dependencies
|
|
48
|
+
- Updated dependencies
|
|
49
|
+
- Updated dependencies
|
|
50
|
+
- Updated dependencies
|
|
51
|
+
- Updated dependencies
|
|
52
|
+
- Updated dependencies
|
|
53
|
+
- Updated dependencies
|
|
54
|
+
- Updated dependencies
|
|
55
|
+
- Updated dependencies
|
|
56
|
+
- Updated dependencies
|
|
57
|
+
- Updated dependencies
|
|
58
|
+
- Updated dependencies
|
|
59
|
+
- Updated dependencies
|
|
60
|
+
- @fungi.computer/agar@0.3.0
|
|
61
|
+
- @fungi.computer/braid@0.2.0
|
|
62
|
+
- @fungi.computer/cairn@0.1.0
|
|
63
|
+
- @fungi.computer/caps@0.2.0
|
|
64
|
+
- @fungi.computer/gills@0.2.0
|
|
65
|
+
- @fungi.computer/issues@0.1.0
|
|
66
|
+
- @fungi.computer/maosaico@0.1.0
|
|
67
|
+
- @fungi.computer/nacre@0.0.1
|
|
68
|
+
- @fungi.computer/oyster@0.3.0
|
|
69
|
+
- @fungi.computer/puffball@0.2.0
|
|
70
|
+
- @fungi.computer/sigils@0.2.0
|
|
71
|
+
- @fungi.computer/stipe@0.1.0
|
|
72
|
+
- @fungi.computer/trellis@0.2.0
|
|
73
|
+
- @fungi.computer/watchdog@0.1.0
|
|
74
|
+
- @fungi.computer/workflows-wire@0.2.0
|
|
75
|
+
|
|
3
76
|
## 0.1.0
|
|
4
77
|
|
|
5
78
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -112,9 +112,10 @@ retried after 10 s, 1 min, 5 min, 30 min and 2 h;
|
|
|
112
112
|
|
|
113
113
|
Bonsai also edits the other Apps on your Oyster. `apps.fork` copies an installed
|
|
114
114
|
App's source into a new Bonsai repository you own. Push your changes to it, then
|
|
115
|
-
open the repository's **Settings** and use **Build & install**:
|
|
116
|
-
that exact commit on
|
|
117
|
-
|
|
115
|
+
open the repository's **Settings** and use **Build & install**: the retained
|
|
116
|
+
Workflow builds that exact commit on a hosted Computer, verifies its Release,
|
|
117
|
+
and installs it or switches a live installation while keeping its data. To fork
|
|
118
|
+
Bonsai itself:
|
|
118
119
|
|
|
119
120
|
```sh
|
|
120
121
|
curl -X POST -H "Authorization: Bearer $OYSTER_OWNER_KEY" -H 'content-type: application/json' \
|
|
@@ -129,6 +130,59 @@ your forks with `--repositories <base URL>` and `--ref <branch or tag>`. The
|
|
|
129
130
|
installer reads all seven starter Apps from that base, so fork them under one
|
|
130
131
|
account.
|
|
131
132
|
|
|
133
|
+
## Make a repository an App
|
|
134
|
+
|
|
135
|
+
On a standalone Oyster, open the repository's **Settings → Make this an App**.
|
|
136
|
+
The owner-only `apps.register({ repo })` call reads the committed main root's
|
|
137
|
+
`package.json` through Oyster's shared manifest reader. The App owns its
|
|
138
|
+
`fungi.appId` identity and `fungi.build` recipe; a registered fork keeps its own
|
|
139
|
+
identity. Registration neither builds nor installs and never selects or starts a
|
|
140
|
+
Computer. Caller-supplied identities, recipes and source installations are not
|
|
141
|
+
registration inputs.
|
|
142
|
+
|
|
143
|
+
**Build & install** fills the readonly App ID and editable revision/name from
|
|
144
|
+
registration or the latest recorded job. Accepted main pushes and merges use the
|
|
145
|
+
host's accepted-ref journal, not a trusted App merge payload, to build the exact
|
|
146
|
+
commit on a hosted backend. A failed build leaves the running Release unchanged.
|
|
147
|
+
Automatic installation reuses approval only: new permissions show **Needs owner
|
|
148
|
+
consent** without switching Releases or offering a misleading install button.
|
|
149
|
+
|
|
150
|
+
The explicit owner button calls `apps.startBuildWorkflow` with the repository,
|
|
151
|
+
revision, installation name and a replay key. It starts the same published
|
|
152
|
+
build/verify/install definition as accepted main pushes; the panel does not call
|
|
153
|
+
`apps.build` or `apps.install` separately. `apps.buildWorkflowRun({ runId })`
|
|
154
|
+
reads its recorded steps for the shared Trellis cards. To discover automatic
|
|
155
|
+
runs without a browser-held ID, the owner can call `apps.buildWorkflowRuns({})`:
|
|
156
|
+
it reads at most fifty stock build Workflow runs from this installation's
|
|
157
|
+
`WORKFLOWS.runs` door. Neither read starts work or maintains a trigger ledger.
|
|
158
|
+
|
|
159
|
+
Settings discovers job status by `apps.buildStatus({ repo })` after reload; no
|
|
160
|
+
remembered browser job ID is required. No recorded job returns `null`, not an
|
|
161
|
+
invented success or failure. Workflows still poll their specific job with
|
|
162
|
+
`apps.buildStatus({ jobId })`; that call is Pending until build and automatic
|
|
163
|
+
installation settle. These are two current selectors for different callers. A
|
|
164
|
+
local-only Computer is not a build backend and returns typed `no_backend`. This
|
|
165
|
+
flow does not deploy a host or implement a separate consent ledger.
|
|
166
|
+
|
|
167
|
+
The retained Trellis **Build on merge** template is requested through the
|
|
168
|
+
Workflows capability by Bonsai's host-attested accepted-main lifecycle, with the
|
|
169
|
+
registered App identity and the exact accepted head. Its verification step is
|
|
170
|
+
**apps.verifyBuild({ jobId, sourceRevision })**: it waits only for the build,
|
|
171
|
+
not for installation, then returns the canonically decoded Release only if its
|
|
172
|
+
provenance matches. Build, verification and install each declare a result card.
|
|
173
|
+
The template has no review configuration or review step; installation requires
|
|
174
|
+
the Workflow's authority, not an App-origin installation exception.
|
|
175
|
+
|
|
176
|
+
**Breaking declaration contract:** Bonsai declares **WORKFLOWS.catalog**,
|
|
177
|
+
**WORKFLOWS.runGraph**, **WORKFLOWS.graphStatus**, **WORKFLOWS.runs**, Workflow
|
|
178
|
+
run/publication capabilities and **APPS.repository**. The latter is a read-only
|
|
179
|
+
lookup for the owner action or **onGitPush**, scoped to the live calling
|
|
180
|
+
installation; it neither registers a source nor selects a Computer. The retained
|
|
181
|
+
runGraph compiler requires catalog permission and reads the Apps bound by the
|
|
182
|
+
request; Bonsai declares that read operation rather than bypassing the host's
|
|
183
|
+
check. Updated declarations must go through the ordinary owner-consent path. Old
|
|
184
|
+
App-origin installation remains refused.
|
|
185
|
+
|
|
132
186
|
## Agent tools
|
|
133
187
|
|
|
134
188
|
Every method is `POST $OYSTER_URL/apps/bonsai/methods/<name>` with a JSON body
|
|
@@ -136,15 +190,15 @@ and a key, and the same methods reach agents through Oyster's MCP endpoint; see
|
|
|
136
190
|
[Git, keys and MCP](https://github.com/fungi-computer/oyster#git-keys-and-mcp).
|
|
137
191
|
Bonsai's own UI uses the same API.
|
|
138
192
|
|
|
139
|
-
| Area | Methods
|
|
140
|
-
| ------------ |
|
|
141
|
-
| Repositories | `repos.list`, `repos.get`, `repos.create`, `repos.fork`, `repos.delete`, `branches.options`
|
|
142
|
-
| Files | `files.tree`, `files.list`, `files.read`, `files.save`, `files.sync`, `files.discard`, `issues.read`
|
|
143
|
-
| History | `commits.list`, `commits.get`, `graph.get`, `activity.list`, `actors.list`
|
|
144
|
-
| Changes | `changes.list`, `changes.get`, `changes.open`, `changes.approve`, `changes.merge`, `changes.close`, `sync`
|
|
145
|
-
| Checks | `checks.request`, `checks.report`
|
|
146
|
-
| Insights | `insights.code`, `insights.layout.get`, `insights.layout.save`
|
|
147
|
-
| Apps (owner) | `apps.fork`, `apps.build`, `apps.buildStatus`, `apps.install`
|
|
193
|
+
| Area | Methods |
|
|
194
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| Repositories | `repos.list`, `repos.get`, `repos.create`, `repos.fork`, `repos.delete`, `branches.options` |
|
|
196
|
+
| Files | `files.tree`, `files.list`, `files.read`, `files.save`, `files.sync`, `files.discard`, `issues.read` |
|
|
197
|
+
| History | `commits.list`, `commits.get`, `graph.get`, `activity.list`, `actors.list` |
|
|
198
|
+
| Changes | `changes.list`, `changes.get`, `changes.open`, `changes.approve`, `changes.merge`, `changes.close`, `sync` |
|
|
199
|
+
| Checks | `checks.request`, `checks.report` |
|
|
200
|
+
| Insights | `insights.code`, `insights.layout.get`, `insights.layout.save` |
|
|
201
|
+
| Apps (owner) | `apps.register`, `apps.fork`, `apps.startBuildWorkflow`, `apps.buildWorkflowRuns`, `apps.buildWorkflowRun`, `apps.build`, `apps.buildStatus`, `apps.verifyBuild`, `apps.install` |
|
|
148
202
|
|
|
149
203
|
## How it is built
|
|
150
204
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as S from "effect/Schema";
|
|
2
|
+
import { parseSha256Digest } from "@fungi.computer/puffball";
|
|
3
|
+
import {
|
|
4
|
+
appGitPushFactSchema,
|
|
5
|
+
type AppGitPushFact,
|
|
6
|
+
} from "@fungi.computer/agar/git";
|
|
7
|
+
import { contentDigest, workflowDefinitionSchema } from "@fungi.computer/braid";
|
|
8
|
+
import type { GraphStartRequest } from "@fungi.computer/braid/runtime";
|
|
9
|
+
import {
|
|
10
|
+
repositoryWorkflowSourceSchema,
|
|
11
|
+
runReply,
|
|
12
|
+
statusReply,
|
|
13
|
+
runsReplySchema,
|
|
14
|
+
} from "@fungi.computer/workflows-wire";
|
|
15
|
+
import { AppMethodRefusal } from "@fungi.computer/agar/backend";
|
|
16
|
+
import type { AppBuildWorkflowInput } from "../shared/contract.js";
|
|
17
|
+
import template from "@fungi.computer/trellis/templates/bonsai-build-on-merge" with { type: "json" };
|
|
18
|
+
|
|
19
|
+
export type RepositoryWorkflowSource =
|
|
20
|
+
typeof repositoryWorkflowSourceSchema.Type;
|
|
21
|
+
export type BuildWorkflowRunReply = ReturnType<typeof runReply.parse>;
|
|
22
|
+
|
|
23
|
+
export type BuildWorkflowBindings = Readonly<{
|
|
24
|
+
APPS?: Readonly<{
|
|
25
|
+
repository(input: {
|
|
26
|
+
repo: string;
|
|
27
|
+
}): Promise<RepositoryWorkflowSource | null>;
|
|
28
|
+
}>;
|
|
29
|
+
WORKFLOWS?: Readonly<{
|
|
30
|
+
runs(input: {
|
|
31
|
+
workflowId?: string;
|
|
32
|
+
}): Promise<ReturnType<typeof runsReplySchema.parse>>;
|
|
33
|
+
runGraph(input: GraphStartRequest): Promise<BuildWorkflowRunReply>;
|
|
34
|
+
graphStatus(input: {
|
|
35
|
+
runId: string;
|
|
36
|
+
}): Promise<ReturnType<typeof statusReply.parse>>;
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
/** One request path for accepted main pushes and an explicit owner action.
|
|
41
|
+
* The host owns lifecycle identity; manual request IDs are ordinary call replay
|
|
42
|
+
* keys. Neither entrypoint constructs a call frame or installs as this App. */
|
|
43
|
+
async function requestBuildWorkflow(
|
|
44
|
+
env: BuildWorkflowBindings,
|
|
45
|
+
input: AppBuildWorkflowInput,
|
|
46
|
+
) {
|
|
47
|
+
if (env.APPS === undefined)
|
|
48
|
+
throw new AppMethodRefusal(
|
|
49
|
+
"unavailable",
|
|
50
|
+
"App source lookup is unavailable",
|
|
51
|
+
);
|
|
52
|
+
const source = S.decodeUnknownSync(S.NullOr(repositoryWorkflowSourceSchema))(
|
|
53
|
+
await env.APPS.repository({ repo: input.repo }),
|
|
54
|
+
);
|
|
55
|
+
if (source === null) return null;
|
|
56
|
+
if (env.WORKFLOWS === undefined)
|
|
57
|
+
throw new AppMethodRefusal("unavailable", "Workflow access is not granted");
|
|
58
|
+
const stock = S.decodeUnknownSync(workflowDefinitionSchema)(
|
|
59
|
+
template.definition,
|
|
60
|
+
);
|
|
61
|
+
const definition = {
|
|
62
|
+
...stock,
|
|
63
|
+
apps: { bonsai: { appId: source.callerAppId } },
|
|
64
|
+
};
|
|
65
|
+
const response = runReply.parse(
|
|
66
|
+
await env.WORKFLOWS.runGraph({
|
|
67
|
+
requestId: input.requestId,
|
|
68
|
+
definition,
|
|
69
|
+
bindings: { bonsai: source.installationId },
|
|
70
|
+
input: {
|
|
71
|
+
appId: source.appId,
|
|
72
|
+
sourceRevision: input.sourceRevision,
|
|
73
|
+
installationId: input.installationId,
|
|
74
|
+
},
|
|
75
|
+
published: {
|
|
76
|
+
workflowId: template.id,
|
|
77
|
+
commit: source.callerSourceRevision.slice(4),
|
|
78
|
+
digest: parseSha256Digest(await contentDigest(definition)),
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
if (
|
|
83
|
+
!("kind" in response) ||
|
|
84
|
+
(response.kind !== "started" && response.kind !== "replay")
|
|
85
|
+
)
|
|
86
|
+
throw new AppMethodRefusal(
|
|
87
|
+
"kind" in response && response.kind === "invalid"
|
|
88
|
+
? "invalid"
|
|
89
|
+
: "unavailable",
|
|
90
|
+
"Build Workflow refused: " + JSON.stringify(response).slice(0, 2000),
|
|
91
|
+
);
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The host journal supplies the accepted main head, never an App merge event. */
|
|
96
|
+
export async function startBuildWorkflow(
|
|
97
|
+
env: BuildWorkflowBindings,
|
|
98
|
+
raw: AppGitPushFact,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const fact = appGitPushFactSchema.parse(raw);
|
|
101
|
+
const update = fact.updates.find(
|
|
102
|
+
(row) =>
|
|
103
|
+
row.ref === "refs/heads/main" &&
|
|
104
|
+
row.after !== "0".repeat(40) &&
|
|
105
|
+
row.before !== row.after,
|
|
106
|
+
);
|
|
107
|
+
if (update === undefined) return;
|
|
108
|
+
await requestBuildWorkflow(env, {
|
|
109
|
+
repo: fact.repository,
|
|
110
|
+
sourceRevision: "git:" + update.after,
|
|
111
|
+
installationId: fact.repository,
|
|
112
|
+
requestId: fact.eventId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Owner calls use the same retained definition, verification and node authority. */
|
|
117
|
+
export async function startManualBuildWorkflow(
|
|
118
|
+
env: BuildWorkflowBindings,
|
|
119
|
+
input: AppBuildWorkflowInput,
|
|
120
|
+
) {
|
|
121
|
+
const started = await requestBuildWorkflow(env, input);
|
|
122
|
+
if (started === null)
|
|
123
|
+
throw new AppMethodRefusal(
|
|
124
|
+
"not_found",
|
|
125
|
+
"This repository is not registered as an App",
|
|
126
|
+
);
|
|
127
|
+
return started;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Discover host-owned runs after a push, without materializing a trigger ledger. */
|
|
131
|
+
export async function readBuildWorkflowRuns(env: BuildWorkflowBindings) {
|
|
132
|
+
if (env.WORKFLOWS === undefined)
|
|
133
|
+
throw new AppMethodRefusal("unavailable", "Workflow access is not granted");
|
|
134
|
+
return runsReplySchema.parse(
|
|
135
|
+
await env.WORKFLOWS.runs({ workflowId: template.id }),
|
|
136
|
+
);
|
|
137
|
+
}
|
package/backend/index.ts
CHANGED
|
@@ -32,15 +32,20 @@ import type {
|
|
|
32
32
|
BackgroundBinding,
|
|
33
33
|
} from "@fungi.computer/puffball/background-replay";
|
|
34
34
|
import { refuseBackground } from "./background.js";
|
|
35
|
+
import {
|
|
36
|
+
startBuildWorkflow,
|
|
37
|
+
type BuildWorkflowBindings,
|
|
38
|
+
} from "./build-workflow.js";
|
|
35
39
|
|
|
36
40
|
/** The bindings the host grants Bonsai (see the Oyster binding proposal). */
|
|
37
|
-
export type BonsaiEnv =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
export type BonsaiEnv = BuildWorkflowBindings &
|
|
42
|
+
Readonly<{
|
|
43
|
+
GIT: GitBinding;
|
|
44
|
+
EVENTS: EventSink;
|
|
45
|
+
BACKGROUND?: BackgroundBinding;
|
|
46
|
+
/** Fork, build and install Apps; present only on a host that supplies it. */
|
|
47
|
+
APPS?: BonsaiAppsBinding;
|
|
48
|
+
}>;
|
|
44
49
|
|
|
45
50
|
const uiRequest = z.strictObject({
|
|
46
51
|
method: z.string().min(1).max(80),
|
|
@@ -88,6 +93,7 @@ class BonsaiFacet extends DurableObject<BonsaiEnv> {
|
|
|
88
93
|
|
|
89
94
|
async onGitPush(fact: AppGitPushFact): Promise<void> {
|
|
90
95
|
await observePush(this.bonsai, fact);
|
|
96
|
+
await startBuildWorkflow(this.env, fact);
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
authorizeGitPush(raw: GitPushQuestion): Promise<GitPushVerdict> {
|