@fungi.computer/bonsai 0.0.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,97 @@
1
1
  # @fungi.computer/bonsai
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Changes since 0.1.0:
8
+
9
+ - fix(oyster): keep Stage out of the starter release
10
+ - fix(shed): compact the model-facing command reply
11
+ - refactor(shed): send Workflow Git commands without any credential
12
+ - feat: show the Fungi sigil in the Bonsai and Tourtois tab
13
+ - fix(bonsai,oyster)!: share the repository App source wire through
14
+ workflows-wire
15
+ - fix(oyster,bonsai): export the types public signatures use
16
+ - refactor(bonsai): share authoring browser fixtures
17
+ - test(bonsai): follow Workflow-only Settings authoring
18
+ - fix(authoring): preserve Workflow refusals across lifecycle and native RPC
19
+ - Revert "fix(bonsai): remove impossible local App release command"
20
+ - fix(bonsai): remove impossible local App release command
21
+ - chore(b3a): checkpoint native workflow consumers and shared renderer types
22
+ - fix(b3a): align template and native proof consumers
23
+ - chore(b3a): checkpoint manual workflow integration
24
+ - refactor: move repository installation into Workflow nodes
25
+ - feat(apps): discover repository builds and install local Releases
26
+ - feat(apps): retain typed build installation outcomes in Settings
27
+ - test(apps): prove honest build refusals through host and Settings
28
+ - feat(bonsai): add repository App registration method and settings action
29
+ - fix(bonsai): clean up HTTPS Git fixture certificates
30
+ - feat(oyster): derive App build recipes from package metadata
31
+ - refactor(oyster)!: move the shell client into @fungi.computer/nacre
32
+ - refactor(computer)!: move the operation contract into computer-contract
33
+ - feat(oyster): add stage to the starter apps
34
+ - feat(oyster): public release 0.2.0
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies
39
+ - Updated dependencies
40
+ - Updated dependencies
41
+ - Updated dependencies
42
+ - Updated dependencies
43
+ - Updated dependencies
44
+ - Updated dependencies
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
+ - @fungi.computer/agar@0.3.0
54
+ - @fungi.computer/braid@0.2.0
55
+ - @fungi.computer/cairn@0.1.0
56
+ - @fungi.computer/caps@0.2.0
57
+ - @fungi.computer/gills@0.2.0
58
+ - @fungi.computer/issues@0.1.0
59
+ - @fungi.computer/maosaico@0.1.0
60
+ - @fungi.computer/nacre@0.0.1
61
+ - @fungi.computer/oyster@0.3.0
62
+ - @fungi.computer/puffball@0.2.0
63
+ - @fungi.computer/sigils@0.2.0
64
+ - @fungi.computer/stipe@0.1.0
65
+ - @fungi.computer/trellis@0.2.0
66
+ - @fungi.computer/watchdog@0.1.0
67
+ - @fungi.computer/workflows-wire@0.2.0
68
+
69
+ ## 0.1.0
70
+
71
+ ### Minor Changes
72
+
73
+ - Changes since 0.0.1:
74
+
75
+ - fix(agar): keep the result-row decoder out of Apps that do not present
76
+ - refactor(caps): one status bead, the gooey one, everywhere
77
+ - feat(shed): one Ghostty terminal for the light and full shells
78
+ - test(bonsai): cheaper real-git tests and a real-git vitest project
79
+ - feat(apps): describe finished Prints, Inkcap, Shed and Bonsai calls
80
+ - feat(oyster): public release 0.1.0
81
+
82
+ ### Patch Changes
83
+
84
+ - Updated dependencies
85
+ - Updated dependencies
86
+ - Updated dependencies
87
+ - Updated dependencies
88
+ - Updated dependencies
89
+ - @fungi.computer/agar@0.2.0
90
+ - @fungi.computer/caps@0.1.0
91
+ - @fungi.computer/gills@0.1.0
92
+ - @fungi.computer/oyster@0.2.0
93
+ - @fungi.computer/puffball@0.1.0
94
+
3
95
  ## 0.0.1
4
96
 
5
97
  ### Patch 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**: Oyster builds
116
- that exact commit on your Computer and installs the Release, or switches a live
117
- installation onto it while keeping its data. To fork Bonsai itself:
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 = Readonly<{
38
- GIT: GitBinding;
39
- EVENTS: EventSink;
40
- BACKGROUND?: BackgroundBinding;
41
- /** Fork, build and install Apps; present only on a host that supplies it. */
42
- APPS?: BonsaiAppsBinding;
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> {