@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 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**: 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> {