@fungi.computer/oyster 0.2.0 → 0.3.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/README.md +396 -72
- package/dist/agents.d.ts +3 -2
- package/dist/agents.js +4 -2
- package/dist/app-builds.d.ts +81 -8
- package/dist/app-builds.js +213 -60
- package/dist/app-storage.d.ts +3 -3
- package/dist/approval-capability.d.ts +5 -0
- package/dist/approval-capability.js +46 -0
- package/dist/approval-home.d.ts +51 -0
- package/dist/approval-home.js +226 -0
- package/dist/apps/catalog.d.ts +335 -0
- package/dist/apps/catalog.js +46 -0
- package/dist/apps/gate.d.ts +20 -6
- package/dist/apps/gate.js +42 -16
- package/dist/apps/index.d.ts +3 -1
- package/dist/apps/index.js +3 -1
- package/dist/apps/installation.d.ts +132 -51
- package/dist/apps/installation.js +176 -70
- package/dist/apps/permissions.d.ts +247 -0
- package/dist/apps/permissions.js +255 -0
- package/dist/boot-capability-route.d.ts +15 -0
- package/dist/boot-capability-route.js +196 -0
- package/dist/boot-capability.d.ts +103 -0
- package/dist/boot-capability.js +141 -0
- package/dist/boot-construction.d.ts +12 -0
- package/dist/boot-construction.js +175 -0
- package/dist/boot-gate.d.ts +267 -0
- package/dist/boot-gate.js +236 -0
- package/dist/boot-host-script.d.ts +2 -0
- package/dist/boot-host-script.js +1 -0
- package/dist/boot-mount.d.ts +21 -0
- package/dist/boot-mount.js +65 -0
- package/dist/browser/boot.d.ts +73 -0
- package/dist/browser/boot.js +502 -0
- package/dist/browser/browser.d.ts +104 -0
- package/dist/browser/browser.js +374 -0
- package/dist/browser/capability-source.d.ts +76 -0
- package/dist/browser/capability-source.js +338 -0
- package/dist/browser/cdp-client.d.ts +56 -0
- package/dist/browser/cdp-client.js +184 -0
- package/dist/browser/errors.d.ts +79 -0
- package/dist/browser/errors.js +32 -0
- package/dist/browser/index.d.ts +16 -0
- package/dist/browser/index.js +14 -0
- package/dist/browser/lease-registry.d.ts +61 -0
- package/dist/browser/lease-registry.js +101 -0
- package/dist/browser/operations.d.ts +225 -0
- package/dist/browser/operations.js +137 -0
- package/dist/browser/page.d.ts +25 -0
- package/dist/browser/page.js +172 -0
- package/dist/browser/quick-reads.d.ts +29 -0
- package/dist/browser/quick-reads.js +160 -0
- package/dist/browser/sessions.d.ts +52 -0
- package/dist/browser/sessions.js +118 -0
- package/dist/browser/url-policy.d.ts +18 -0
- package/dist/browser/url-policy.js +58 -0
- package/dist/build/app-imports.d.ts +12 -0
- package/dist/build/app-imports.js +105 -0
- package/dist/build/build-policy.d.ts +5 -3
- package/dist/build/build-policy.js +5 -3
- package/dist/build/candidate-builder.d.ts +1 -1
- package/dist/build/candidate-cli.mjs +8375 -334
- package/dist/build/local-build.d.ts +2 -0
- package/dist/build/local-build.js +28 -6
- package/dist/build/snapshot-package-store.d.ts +1 -1
- package/dist/build/snapshot-package-store.js +7 -2
- package/dist/build/workspace-build.d.ts +92 -5
- package/dist/build/workspace-build.js +93 -19
- package/dist/build.d.ts +1 -1
- package/dist/build.js +1 -1
- package/dist/catalog-frame.js +1 -1
- package/dist/cloudflare.d.ts +13 -3
- package/dist/cloudflare.js +102 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.js +4 -2
- package/dist/git-auth.d.ts +0 -2
- package/dist/git-auth.js +0 -2
- package/dist/git.d.ts +0 -1
- package/dist/git.js +0 -2
- package/dist/host.d.ts +39 -13
- package/dist/host.js +268 -142
- package/dist/index.d.ts +25 -15
- package/dist/index.js +39 -16
- package/dist/install-release-cli.d.ts +2 -0
- package/dist/install-release-cli.js +46 -0
- package/dist/installation-client.d.ts +43 -0
- package/dist/installation-client.js +178 -0
- package/dist/installation-execution.d.ts +11 -0
- package/dist/installation-execution.js +184 -0
- package/dist/installation-operations.d.ts +1437 -0
- package/dist/installation-operations.js +664 -0
- package/dist/installation-store-generation.d.ts +3 -0
- package/dist/installation-store-generation.js +16 -0
- package/dist/instance-id.d.ts +10 -0
- package/dist/instance-id.js +10 -0
- package/dist/instance-refusal.d.ts +11 -0
- package/dist/instance-refusal.js +21 -0
- package/dist/internal/delivery.js +4 -2
- package/dist/internal/host-gate.js +7 -1
- package/dist/internal/registry-installations.js +4 -4
- package/dist/keys.d.ts +5 -1
- package/dist/keys.js +10 -2
- package/dist/machine-link/authority.d.ts +89 -0
- package/dist/machine-link/authority.js +37 -0
- package/dist/machine-link/client.d.ts +66 -0
- package/dist/machine-link/client.js +94 -0
- package/dist/machine-link/delivery.d.ts +18 -0
- package/dist/machine-link/delivery.js +55 -0
- package/dist/machine-link/enrollment.d.ts +111 -0
- package/dist/machine-link/enrollment.js +286 -0
- package/dist/machine-link/fences.d.ts +37 -0
- package/dist/machine-link/fences.js +166 -0
- package/dist/machine-link/home.d.ts +20 -0
- package/dist/machine-link/home.js +24 -0
- package/dist/machine-link/host.d.ts +43 -0
- package/dist/machine-link/host.js +189 -0
- package/dist/machine-link/index.d.ts +15 -0
- package/dist/machine-link/index.js +15 -0
- package/dist/machine-link/link.d.ts +48 -0
- package/dist/machine-link/link.js +223 -0
- package/dist/machine-link/presence.d.ts +10 -0
- package/dist/machine-link/presence.js +25 -0
- package/dist/machine-link/workers.d.ts +6 -0
- package/dist/machine-link/workers.js +6 -0
- package/dist/mcp-owner.d.ts +2 -2
- package/dist/mcp-owner.js +41 -33
- package/dist/mcp.d.ts +15 -3
- package/dist/mcp.js +146 -25
- package/dist/older-oyster-store-refusal.d.ts +4 -0
- package/dist/older-oyster-store-refusal.js +18 -0
- package/dist/older-oyster-store.d.ts +21 -0
- package/dist/older-oyster-store.js +43 -0
- package/dist/pack.d.ts +14 -3
- package/dist/pack.js +44 -19
- package/dist/page/app-access.d.ts +7 -0
- package/dist/page/app-access.js +75 -0
- package/dist/page/boot-host.js +57 -0
- package/dist/page/capabilities.d.ts +3 -3
- package/dist/page/capabilities.js +16 -12
- package/dist/page/computer-handles.d.ts +3 -3
- package/dist/page/computer-target.d.ts +3 -0
- package/dist/page/computer-target.js +9 -0
- package/dist/page/mount.d.ts +23 -3
- package/dist/page/mount.js +152 -16
- package/dist/page-assets.d.ts +22 -0
- package/dist/page-assets.js +28 -0
- package/dist/page-script.d.ts +3 -0
- package/dist/page-script.js +5 -4
- package/dist/release.d.ts +13 -6
- package/dist/release.js +10 -7
- package/dist/root-data.d.ts +16 -6
- package/dist/root-data.js +2 -2
- package/dist/source/index.d.ts +1 -1
- package/dist/source/team-source.d.ts +20 -8
- package/dist/source/team-source.js +39 -10
- package/dist/source/template-seed.d.ts +2 -2
- package/dist/standalone/agent-mail.d.ts +24 -0
- package/dist/standalone/agent-mail.js +62 -0
- package/dist/standalone/agent-store.d.ts +9 -0
- package/dist/standalone/agent-store.js +30 -0
- package/dist/standalone/agents.d.ts +68 -9
- package/dist/standalone/agents.js +428 -56
- package/dist/standalone/app-builds-binding.d.ts +20 -2
- package/dist/standalone/app-builds-binding.js +147 -23
- package/dist/standalone/app-set.d.ts +5 -2
- package/dist/standalone/app-set.js +186 -144
- package/dist/standalone/browser.d.ts +11 -0
- package/dist/standalone/browser.js +72 -0
- package/dist/standalone/builds.d.ts +13 -4
- package/dist/standalone/builds.js +28 -44
- package/dist/standalone/computer-egress.d.ts +2 -0
- package/dist/standalone/computer-egress.js +13 -4
- package/dist/standalone/computer-fleet.d.ts +43 -0
- package/dist/standalone/computer-fleet.js +200 -0
- package/dist/standalone/computer.d.ts +44 -9
- package/dist/standalone/computer.js +129 -22
- package/dist/standalone/install-apps-cli.js +43 -6
- package/dist/standalone/instance.d.ts +2 -2
- package/dist/standalone/instance.js +8 -11
- package/dist/standalone/machine-link.d.ts +354 -0
- package/dist/standalone/machine-link.js +368 -0
- package/dist/standalone/media.d.ts +2 -2
- package/dist/standalone/media.js +19 -16
- package/dist/standalone/preview.d.ts +2 -2
- package/dist/standalone/preview.js +37 -10
- package/dist/standalone/registry.d.ts +151 -9
- package/dist/standalone/registry.js +305 -8
- package/dist/standalone/worker.d.ts +19 -5
- package/dist/standalone/worker.js +90 -54
- package/dist/standalone/workflow-binding.d.ts +18 -22
- package/dist/standalone/workflow-binding.js +18 -18
- package/dist/standalone/workflow-control.d.ts +4 -0
- package/dist/standalone/workflow-control.js +40 -0
- package/dist/standalone/workflow-events.d.ts +2 -2
- package/dist/standalone/workflow-events.js +9 -10
- package/dist/standalone/workflow-git-capture.js +4 -3
- package/dist/standalone/workflow-host.d.ts +162 -161
- package/dist/standalone/workflow-host.js +101 -21
- package/dist/standalone/workflow-node-capture.js +11 -4
- package/dist/standalone/workflow-schedules.d.ts +5 -2
- package/dist/standalone/workflow-schedules.js +9 -10
- package/dist/standalone/workflow-trigger-host.d.ts +65 -5
- package/dist/standalone/workflow-trigger-host.js +2 -2
- package/dist/standalone/workflow-webhooks.d.ts +22 -0
- package/dist/standalone/workflow-webhooks.js +39 -0
- package/dist/standalone/workflows.d.ts +140 -43
- package/dist/standalone/workflows.js +198 -39
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.js +14 -3
- package/dist/testing/installation.js +26 -2
- package/dist/testing/worker.js +1 -1
- package/dist/testing/workers-runtime.d.ts +8 -2
- package/dist/testing/workers-runtime.js +20 -2
- package/dist/webhook-abuse.d.ts +25 -0
- package/dist/webhook-abuse.js +54 -0
- package/dist/webhook-custody.d.ts +144 -0
- package/dist/webhook-custody.js +222 -0
- package/dist/webhook-ingress.d.ts +36 -0
- package/dist/webhook-ingress.js +146 -0
- package/dist/webhooks.d.ts +111 -0
- package/dist/webhooks.js +189 -0
- package/dist/workers-git.d.ts +12 -17
- package/dist/workers-git.js +64 -52
- package/dist/workers.d.ts +226 -13
- package/dist/workers.js +363 -51
- package/dist/workflow-agent-authority.d.ts +8 -0
- package/dist/workflow-agent-authority.js +42 -0
- package/dist/workflows.d.ts +66 -0
- package/dist/workflows.js +97 -0
- package/docs/api/README.md +63 -37
- package/docs/api/agents.md +17 -0
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +1394 -543
- package/docs/api/browser/boot.md +132 -0
- package/docs/api/browser.md +1440 -0
- package/docs/api/build-contract.md +38 -25
- package/docs/api/build.md +263 -84
- package/docs/api/cloudflare.md +93 -7
- package/docs/api/git.md +3 -13
- package/docs/api/host.md +944 -118
- package/docs/api/index.md +320 -18
- package/docs/api/keys.md +24 -1
- package/docs/api/mcp.md +59 -12
- package/docs/api/modules.md +33 -40
- package/docs/api/pack.md +57 -8
- package/docs/api/page/mount.md +92 -4
- package/docs/api/workers-git.md +103 -24
- package/docs/api/workers.md +1594 -244
- package/docs/site/agent-quickstart.mdx +28 -14
- package/examples/standalone/worker.ts +3 -1
- package/examples/standalone/wrangler.jsonc +2 -0
- package/package.json +73 -71
- package/dist/installation-consent.d.ts +0 -81
- package/dist/installation-consent.js +0 -84
- package/dist/shell/app-directory.d.ts +0 -542
- package/dist/shell/app-directory.js +0 -196
- package/dist/shell/browser-actions.d.ts +0 -7
- package/dist/shell/browser-actions.js +0 -56
- package/dist/shell/command-host.d.ts +0 -112
- package/dist/shell/command-host.js +0 -502
- package/dist/shell/commands.d.ts +0 -25
- package/dist/shell/commands.js +0 -78
- package/dist/shell/index.d.ts +0 -8
- package/dist/shell/index.js +0 -8
- package/dist/shell/keys.d.ts +0 -41
- package/dist/shell/keys.js +0 -134
- package/dist/shell/shell-module.d.ts +0 -55
- package/dist/shell/shell-module.js +0 -86
- package/dist/shell/window-manager.d.ts +0 -128
- package/dist/shell/window-manager.js +0 -730
- package/dist/shell/workbench-contract.d.ts +0 -68
- package/dist/shell/workbench-directory.d.ts +0 -332
- package/dist/shell/workbench-directory.js +0 -35
- package/dist/shell/workbench-host-types.d.ts +0 -96
- package/dist/shell/workbench-host-types.js +0 -1
- package/dist/shell/workbench-host.d.ts +0 -34
- package/dist/shell/workbench-host.js +0 -352
- package/dist/shell/workbench-model.d.ts +0 -194
- package/dist/shell/workbench-model.js +0 -53
- package/dist/shell/workbench.d.ts +0 -4
- package/dist/shell/workbench.js +0 -2
- package/docs/api/shell/app-directory.md +0 -599
- package/docs/api/shell/command-host.md +0 -224
- package/docs/api/shell/commands.md +0 -197
- package/docs/api/shell/keys.md +0 -183
- package/docs/api/shell/shell-module.md +0 -204
- package/docs/api/shell/window-manager.md +0 -267
- package/docs/api/shell/workbench-directory.md +0 -119
- package/docs/api/shell/workbench.md +0 -205
- package/docs/api/shell.md +0 -1915
- /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
package/dist/app-builds.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import * as Schema from "effect/Schema";
|
|
20
20
|
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
21
21
|
import { type Release } from "@fungi.computer/puffball";
|
|
22
|
+
import { type AppPublisherCoreConfigInput } from "./build/effect.js";
|
|
22
23
|
import { type TeamAppCandidateManifest } from "./build/submission-contract.js";
|
|
23
24
|
import { type ArtifactHeads, type ArtifactStore } from "./release.js";
|
|
24
25
|
import type { PublisherTemplateSeedReceipts } from "./source/seed-receipt.js";
|
|
@@ -77,6 +78,19 @@ export declare const oysterBuildSourceSchema: Schema.Struct<{
|
|
|
77
78
|
readonly buildRecipeDigest: Schema.refine<import("@fungi.computer/puffball").Sha256Digest, Schema.String>;
|
|
78
79
|
}>;
|
|
79
80
|
export type OysterBuildSource = Schema.Schema.Type<typeof oysterBuildSourceSchema>;
|
|
81
|
+
/** Installation is a build outcome, never a pending consent/approval record. */
|
|
82
|
+
export declare const oysterBuildInstallationSchema: Schema.Union<readonly [Schema.Struct<{
|
|
83
|
+
readonly kind: Schema.Literal<"pending">;
|
|
84
|
+
readonly installationId: Schema.String;
|
|
85
|
+
}>, Schema.Struct<{
|
|
86
|
+
readonly kind: Schema.Literals<readonly ["installed", "needs_consent"]>;
|
|
87
|
+
readonly installationId: Schema.String;
|
|
88
|
+
}>, Schema.Struct<{
|
|
89
|
+
readonly kind: Schema.Literal<"failed">;
|
|
90
|
+
readonly installationId: Schema.String;
|
|
91
|
+
readonly detail: Schema.String;
|
|
92
|
+
}>]>;
|
|
93
|
+
export type OysterBuildInstallation = Schema.Schema.Type<typeof oysterBuildInstallationSchema>;
|
|
80
94
|
/** What an owner sees of one build. */
|
|
81
95
|
export type OysterBuildView = Readonly<{
|
|
82
96
|
jobId: string;
|
|
@@ -86,8 +100,42 @@ export type OysterBuildView = Readonly<{
|
|
|
86
100
|
status: "running" | "succeeded" | "failed" | "revoked";
|
|
87
101
|
detail: string | null;
|
|
88
102
|
release: Release | null;
|
|
103
|
+
installation: OysterBuildInstallation | null;
|
|
104
|
+
}>;
|
|
105
|
+
/** Known preflight refusals, before a job or Computer run exists. */
|
|
106
|
+
export declare const oysterBuildStartReasonSchema: Schema.Literals<readonly ["invalid_revision", "no_source", "not_template_seed", "no_backend"]>;
|
|
107
|
+
export type OysterBuildStartReason = Schema.Schema.Type<typeof oysterBuildStartReasonSchema>;
|
|
108
|
+
export declare const oysterBuildStartMessages: {
|
|
109
|
+
invalid_revision: string;
|
|
110
|
+
no_source: string;
|
|
111
|
+
no_backend: string;
|
|
112
|
+
not_template_seed: string;
|
|
113
|
+
};
|
|
114
|
+
declare const OysterBuildStartError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
115
|
+
readonly _tag: "OysterBuildStartError";
|
|
116
|
+
} & Readonly<A>;
|
|
117
|
+
/** Local typed failure. The registry turns it into data before a DO RPC. */
|
|
118
|
+
export declare class OysterBuildStartError extends OysterBuildStartError_base<{
|
|
119
|
+
readonly reason: OysterBuildStartReason;
|
|
120
|
+
readonly message: string;
|
|
121
|
+
}> {
|
|
122
|
+
constructor(reason: OysterBuildStartReason);
|
|
123
|
+
}
|
|
124
|
+
export type OysterBuildStartResult = Readonly<{
|
|
125
|
+
kind: "started";
|
|
126
|
+
build: OysterBuildView;
|
|
127
|
+
}> | Readonly<{
|
|
128
|
+
kind: "refused";
|
|
129
|
+
reason: OysterBuildStartReason;
|
|
130
|
+
}>;
|
|
131
|
+
export type OysterBuildBackend = Readonly<{
|
|
132
|
+
kind: "hosted";
|
|
133
|
+
}> | Readonly<{
|
|
134
|
+
kind: "no_backend";
|
|
89
135
|
}>;
|
|
90
136
|
export type OysterBuildComputer = Readonly<{
|
|
137
|
+
/** Read the configured hosted route without starting a machine. Local machines never build Apps. */
|
|
138
|
+
buildBackend(): Promise<OysterBuildBackend>;
|
|
91
139
|
/** Run the fixed build command; the Computer re-admits the job first.
|
|
92
140
|
* A missing `output` means no terminal result was established: the
|
|
93
141
|
* command may still be running, so it is never read as an exit. */
|
|
@@ -118,17 +166,28 @@ export type OysterAppBuildOptions = Readonly<{
|
|
|
118
166
|
templates: Pick<PublisherTemplateSeedReceipts, "read">;
|
|
119
167
|
now?: () => number;
|
|
120
168
|
}>;
|
|
121
|
-
/**
|
|
122
|
-
* The SQLite tables one Oyster build owner keeps. Creation and any carrying
|
|
123
|
-
* forward of retained facts run as one transaction of the owner's storage, so
|
|
124
|
-
* an interrupted conversion leaves the facts as they were for a retry.
|
|
125
|
-
*/
|
|
169
|
+
/** Create the current build tables and index atomically; an older store is refused, never converted. */
|
|
126
170
|
export declare function migrateOysterAppBuilds(owner: WatchdogSqliteOwner): void;
|
|
127
171
|
/** The build owner over one tenant's SQLite, Computer and artifact store. */
|
|
128
172
|
export declare function createOysterAppBuilds(options: OysterAppBuildOptions): Readonly<{
|
|
129
173
|
/** Record (or replace) an App's build source; the caller is the owner. */
|
|
130
|
-
registerSource(source: OysterBuildSource)
|
|
174
|
+
registerSource: (source: OysterBuildSource) => OysterBuildSource;
|
|
175
|
+
/** Resolve repository identity and replace its current recipe as one transition.
|
|
176
|
+
* A fork's existing identity wins over the copied manifest's App id. */
|
|
177
|
+
registerRepositorySource(input: {
|
|
178
|
+
appId: string;
|
|
179
|
+
location: Extract<OysterBuildLocation, {
|
|
180
|
+
kind: "installation";
|
|
181
|
+
}>;
|
|
182
|
+
recipe: AppPublisherCoreConfigInput["recipe"];
|
|
183
|
+
}): {
|
|
184
|
+
kind: "registered";
|
|
185
|
+
source: OysterBuildSource;
|
|
186
|
+
} | {
|
|
187
|
+
kind: "conflict";
|
|
188
|
+
};
|
|
131
189
|
readSource: (appId: string) => OysterBuildSource | null;
|
|
190
|
+
readRepositorySource: (installationId: string, repository: string) => OysterBuildSource | null;
|
|
132
191
|
/**
|
|
133
192
|
* Mint one build job for an App's registered source at one exact commit.
|
|
134
193
|
* A template source builds only a seed revision recorded for its pinned
|
|
@@ -136,15 +195,28 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
|
|
|
136
195
|
* Answers the job and its one run, which the caller keeps alive (the
|
|
137
196
|
* upload grant exists only inside that run's fixed command).
|
|
138
197
|
*/
|
|
139
|
-
start(input: {
|
|
198
|
+
start: (input: {
|
|
140
199
|
appId: string;
|
|
141
200
|
sourceRevision: string;
|
|
142
201
|
requestedBy: string;
|
|
143
|
-
|
|
202
|
+
/** The Workflow's installation target. Absence leaves manual builds manual. */
|
|
203
|
+
installAt?: string;
|
|
204
|
+
/** One host-owned Workflow node reuses its job, including after restart. */
|
|
205
|
+
requestId?: string;
|
|
206
|
+
}) => Promise<Readonly<{
|
|
144
207
|
build: OysterBuildView;
|
|
145
208
|
run: () => Promise<OysterBuildView>;
|
|
146
209
|
}>>;
|
|
147
210
|
read(jobId: string): OysterBuildView | null;
|
|
211
|
+
/** Discover the current source's latest job from the caller's Git owner,
|
|
212
|
+
* never a job id remembered in a browser or another installation's repo. */
|
|
213
|
+
latestRepositoryBuild(installationId: string, repository: string): OysterBuildView | null;
|
|
214
|
+
/** One bounded host installation at a time. Only a verified successful
|
|
215
|
+
* build may reach the installer; the same existing row owns its outcome.
|
|
216
|
+
* A Workflow retry may resume a pending decision, never a terminal one. */
|
|
217
|
+
install(jobId: string, decide: (release: Release, installationId: string) => Promise<Exclude<OysterBuildInstallation, {
|
|
218
|
+
kind: "pending";
|
|
219
|
+
}>>): Promise<boolean>;
|
|
148
220
|
/** A built Release by id, for an owner to install or select. */
|
|
149
221
|
release(releaseId: string): Release | null;
|
|
150
222
|
/** May this Computer execute this job now? Re-checked by the Computer. */
|
|
@@ -208,3 +280,4 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
|
|
|
208
280
|
submitCandidate(jobId: string, manifest: TeamAppCandidateManifest, grant: string, credentialId: string): Promise<Response>;
|
|
209
281
|
}>;
|
|
210
282
|
export type OysterAppBuilds = ReturnType<typeof createOysterAppBuilds>;
|
|
283
|
+
export {};
|
package/dist/app-builds.js
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
* installed one, and records it; installing it is a separate owner act.
|
|
18
18
|
*/
|
|
19
19
|
import * as Schema from "effect/Schema";
|
|
20
|
+
import * as Data from "effect/Data";
|
|
21
|
+
import * as Option from "effect/Option";
|
|
20
22
|
import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
|
|
21
23
|
import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
22
|
-
import { capturedPinnedRecipeSchema, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
|
|
24
|
+
import { capturedPinnedRecipeSchema, normalizeAppPublisherRecipe, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
|
|
23
25
|
import { candidateManifestObjects, decodeCandidateBackend, digestTeamAppCandidateManifest, } from "./build/submission-contract.js";
|
|
24
26
|
import { contentObjectRef } from "./build/content-objects.js";
|
|
25
27
|
import { createTeamBuildPublisher, teamAppBuildCommand, } from "./build/build-jobs.js";
|
|
@@ -27,6 +29,7 @@ import { writeArtifactRoot, } from "./build/artifact-writer.js";
|
|
|
27
29
|
import { buildGitPassPrefix } from "./git-auth.js";
|
|
28
30
|
import { defaultAssetPolicy, readVerified, verifyRelease, } from "./release.js";
|
|
29
31
|
import { appForTemplateRepository, templateRepositoryForApp, } from "./source/template-repository.js";
|
|
32
|
+
import { buildJobsFromOlderOyster, OlderOysterStoreError, } from "./older-oyster-store.js";
|
|
30
33
|
/** A Computer's whole-build budget: a cold build on a 6 GiB Computer measured 185-270 s. */
|
|
31
34
|
export const oysterAppBuildTimeoutMs = 900_000;
|
|
32
35
|
/** The job, its clone admission and its upload grant end together. */
|
|
@@ -52,6 +55,42 @@ export const oysterBuildSourceSchema = Schema.Struct({
|
|
|
52
55
|
recipe: capturedPinnedRecipeSchema,
|
|
53
56
|
buildRecipeDigest: sha256DigestSchema,
|
|
54
57
|
});
|
|
58
|
+
/** Installation is a build outcome, never a pending consent/approval record. */
|
|
59
|
+
export const oysterBuildInstallationSchema = Schema.Union([
|
|
60
|
+
Schema.Struct({
|
|
61
|
+
kind: Schema.Literal("pending"),
|
|
62
|
+
installationId: Schema.String,
|
|
63
|
+
}),
|
|
64
|
+
Schema.Struct({
|
|
65
|
+
kind: Schema.Literals(["installed", "needs_consent"]),
|
|
66
|
+
installationId: Schema.String,
|
|
67
|
+
}),
|
|
68
|
+
Schema.Struct({
|
|
69
|
+
kind: Schema.Literal("failed"),
|
|
70
|
+
installationId: Schema.String,
|
|
71
|
+
detail: Schema.String,
|
|
72
|
+
}),
|
|
73
|
+
]);
|
|
74
|
+
const installationCodec = Schema.fromJsonString(oysterBuildInstallationSchema);
|
|
75
|
+
/** Known preflight refusals, before a job or Computer run exists. */
|
|
76
|
+
export const oysterBuildStartReasonSchema = Schema.Literals([
|
|
77
|
+
"invalid_revision",
|
|
78
|
+
"no_source",
|
|
79
|
+
"not_template_seed",
|
|
80
|
+
"no_backend",
|
|
81
|
+
]);
|
|
82
|
+
export const oysterBuildStartMessages = {
|
|
83
|
+
invalid_revision: "Source revision must be git: followed by a full commit hash",
|
|
84
|
+
no_source: "App has no build source",
|
|
85
|
+
no_backend: "No hosted build backend is attached to this Computer",
|
|
86
|
+
not_template_seed: "Revision is not a seed of this App's template",
|
|
87
|
+
};
|
|
88
|
+
/** Local typed failure. The registry turns it into data before a DO RPC. */
|
|
89
|
+
export class OysterBuildStartError extends Data.TaggedError("OysterBuildStartError") {
|
|
90
|
+
constructor(reason) {
|
|
91
|
+
super({ reason, message: oysterBuildStartMessages[reason] });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
55
94
|
/** One Computer as the build owner reaches it. */
|
|
56
95
|
/** The Computer established no terminal result for a build command, for
|
|
57
96
|
* example because its connection to the container dropped mid-command. */
|
|
@@ -76,10 +115,12 @@ const jobRow = Schema.Struct({
|
|
|
76
115
|
requested_by: Schema.String,
|
|
77
116
|
credential_id: Schema.String,
|
|
78
117
|
upload_grant_digest: Schema.String,
|
|
118
|
+
request_id: Schema.NullOr(Schema.String),
|
|
79
119
|
status: Schema.Literals(["running", "succeeded", "failed", "revoked"]),
|
|
80
120
|
artifact_digest: Schema.NullOr(Schema.String),
|
|
81
121
|
artifact_bytes: Schema.NullOr(Schema.Number),
|
|
82
122
|
detail: Schema.NullOr(Schema.String),
|
|
123
|
+
installation: Schema.NullOr(installationCodec),
|
|
83
124
|
issued_at: Schema.Number,
|
|
84
125
|
expires_at: Schema.Number,
|
|
85
126
|
executable_until: Schema.Number,
|
|
@@ -107,48 +148,23 @@ function sameDigest(left, right) {
|
|
|
107
148
|
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
108
149
|
return difference === 0;
|
|
109
150
|
}
|
|
110
|
-
/**
|
|
111
|
-
* The SQLite tables one Oyster build owner keeps. Creation and any carrying
|
|
112
|
-
* forward of retained facts run as one transaction of the owner's storage, so
|
|
113
|
-
* an interrupted conversion leaves the facts as they were for a retry.
|
|
114
|
-
*/
|
|
151
|
+
/** Create the current build tables and index atomically; an older store is refused, never converted. */
|
|
115
152
|
export function migrateOysterAppBuilds(owner) {
|
|
116
153
|
const { sql } = owner;
|
|
154
|
+
if (buildJobsFromOlderOyster(sql))
|
|
155
|
+
throw new OlderOysterStoreError();
|
|
117
156
|
owner.transactionSync(() => {
|
|
118
157
|
sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_sources (app_id TEXT PRIMARY KEY, source TEXT NOT NULL) STRICT");
|
|
119
|
-
// Sources recorded before the location union named only an installation
|
|
120
|
-
// repository; carry each forward once, in place.
|
|
121
|
-
sql.exec(`UPDATE oyster_build_sources SET source = json_set(
|
|
122
|
-
json_remove(source, '$.installationId', '$.repository'), '$.location',
|
|
123
|
-
json_object('kind', 'installation',
|
|
124
|
-
'installationId', json_extract(source, '$.installationId'),
|
|
125
|
-
'repository', json_extract(source, '$.repository')))
|
|
126
|
-
WHERE json_type(source, '$.location') IS NULL`);
|
|
127
158
|
const jobColumns = `
|
|
128
159
|
job_id TEXT PRIMARY KEY, app_id TEXT NOT NULL, release_id TEXT NOT NULL UNIQUE,
|
|
129
160
|
source_revision TEXT NOT NULL, location TEXT NOT NULL,
|
|
130
161
|
recipe TEXT NOT NULL, recipe_digest TEXT NOT NULL, requested_by TEXT NOT NULL,
|
|
131
162
|
credential_id TEXT NOT NULL, upload_grant_digest TEXT NOT NULL,
|
|
132
163
|
status TEXT NOT NULL, artifact_digest TEXT, artifact_bytes INTEGER, detail TEXT,
|
|
164
|
+
installation TEXT, request_id TEXT,
|
|
133
165
|
issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, executable_until INTEGER NOT NULL`;
|
|
134
166
|
sql.exec(`CREATE TABLE IF NOT EXISTS oyster_build_jobs (${jobColumns}) STRICT`);
|
|
135
|
-
|
|
136
|
-
// in two columns; carry each into the one location column, once.
|
|
137
|
-
if (sql
|
|
138
|
-
.exec("SELECT name FROM pragma_table_info('oyster_build_jobs') WHERE name='installation_id'")
|
|
139
|
-
.toArray().length > 0) {
|
|
140
|
-
sql.exec(`CREATE TABLE oyster_build_jobs_current (${jobColumns}) STRICT`);
|
|
141
|
-
sql.exec(`INSERT INTO oyster_build_jobs_current SELECT job_id, app_id, release_id,
|
|
142
|
-
source_revision,
|
|
143
|
-
json_object('kind', 'installation', 'installationId', installation_id,
|
|
144
|
-
'repository', repository),
|
|
145
|
-
recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
|
|
146
|
-
status, artifact_digest, artifact_bytes, detail, issued_at, expires_at,
|
|
147
|
-
executable_until
|
|
148
|
-
FROM oyster_build_jobs`);
|
|
149
|
-
sql.exec("DROP TABLE oyster_build_jobs");
|
|
150
|
-
sql.exec("ALTER TABLE oyster_build_jobs_current RENAME TO oyster_build_jobs");
|
|
151
|
-
}
|
|
167
|
+
sql.exec("CREATE UNIQUE INDEX IF NOT EXISTS oyster_build_request ON oyster_build_jobs(request_id)");
|
|
152
168
|
sql.exec("CREATE TABLE IF NOT EXISTS oyster_build_releases (release_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE, release TEXT NOT NULL) STRICT");
|
|
153
169
|
});
|
|
154
170
|
}
|
|
@@ -172,7 +188,10 @@ export function createOysterAppBuilds(options) {
|
|
|
172
188
|
// job, so an owner restart must not fail it. A job that reaches its expiry
|
|
173
189
|
// without a result can never settle, so it fails then instead of reading
|
|
174
190
|
// as running for as long as anyone asks.
|
|
175
|
-
const
|
|
191
|
+
const failedInstallation = `CASE WHEN installation IS NULL THEN NULL ELSE
|
|
192
|
+
json_object('kind', 'failed', 'installationId', json_extract(installation, '$.installationId'),
|
|
193
|
+
'detail', COALESCE(?, 'Build did not succeed')) END`;
|
|
194
|
+
const expire = () => owner.sql.exec(`UPDATE oyster_build_jobs SET installation=${failedInstallation}, status='failed', detail=? WHERE status='running' AND expires_at<=?`, "Build expired without a result", "Build expired without a result", now());
|
|
176
195
|
expire();
|
|
177
196
|
const decodeRecipe = (recipe) => Schema.decodeUnknownSync(recipeCodec)(recipe);
|
|
178
197
|
const grantAdmits = async (job, grant) => sameDigest(job.upload_grant_digest, await sha256Hex(new TextEncoder().encode(grant)));
|
|
@@ -209,7 +228,8 @@ export function createOysterAppBuilds(options) {
|
|
|
209
228
|
detail: job.detail,
|
|
210
229
|
});
|
|
211
230
|
const settle = (jobId, status, detail) => {
|
|
212
|
-
owner.sql.exec(
|
|
231
|
+
owner.sql.exec(`UPDATE oyster_build_jobs SET installation=CASE WHEN ?='succeeded' THEN installation ELSE ${failedInstallation} END,
|
|
232
|
+
status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'`, status, detail, status, detail, now(), jobId);
|
|
213
233
|
};
|
|
214
234
|
const required = (jobId) => {
|
|
215
235
|
const job = readJob(jobId);
|
|
@@ -293,16 +313,128 @@ export function createOysterAppBuilds(options) {
|
|
|
293
313
|
status: job.status,
|
|
294
314
|
detail: job.detail,
|
|
295
315
|
release: readRelease(job.job_id),
|
|
316
|
+
installation: job.installation,
|
|
296
317
|
});
|
|
318
|
+
const registerSource = (source) => {
|
|
319
|
+
if (digestPinnedRecipe(source.recipe) !== source.buildRecipeDigest)
|
|
320
|
+
throw new Error("Build recipe digest does not match its recipe");
|
|
321
|
+
owner.sql.exec("INSERT INTO oyster_build_sources(app_id, source) VALUES(?, ?) ON CONFLICT(app_id) DO UPDATE SET source=excluded.source", source.appId, Schema.encodeUnknownSync(sourceCodec)(source));
|
|
322
|
+
return source;
|
|
323
|
+
};
|
|
324
|
+
const readRepositorySource = (installationId, repository) => {
|
|
325
|
+
const [row] = owner.sql
|
|
326
|
+
.exec("SELECT source FROM oyster_build_sources WHERE json_extract(source, '$.location.kind')='installation' AND json_extract(source, '$.location.installationId')=? AND json_extract(source, '$.location.repository')=? LIMIT 1", installationId, repository)
|
|
327
|
+
.toArray();
|
|
328
|
+
return row === undefined
|
|
329
|
+
? null
|
|
330
|
+
: Schema.decodeUnknownSync(sourceCodec)(Schema.decodeUnknownSync(sourceRow)(row).source);
|
|
331
|
+
};
|
|
332
|
+
async function start(input) {
|
|
333
|
+
const revision = Schema.decodeUnknownOption(sourceRevisionSchema)(input.sourceRevision);
|
|
334
|
+
if (Option.isNone(revision))
|
|
335
|
+
throw new OysterBuildStartError("invalid_revision");
|
|
336
|
+
const sourceRevision = revision.value;
|
|
337
|
+
const source = readSource(input.appId);
|
|
338
|
+
if (source === null)
|
|
339
|
+
throw new OysterBuildStartError("no_source");
|
|
340
|
+
if (source.location.kind === "template") {
|
|
341
|
+
const receipt = await options.templates.read(source.appId, sourceRevision);
|
|
342
|
+
if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
|
|
343
|
+
throw new OysterBuildStartError("not_template_seed");
|
|
344
|
+
}
|
|
345
|
+
const reusedRequest = () => {
|
|
346
|
+
if (input.requestId !== undefined) {
|
|
347
|
+
const [row] = owner.sql
|
|
348
|
+
.exec("SELECT * FROM oyster_build_jobs WHERE request_id=?", input.requestId)
|
|
349
|
+
.toArray();
|
|
350
|
+
if (row !== undefined) {
|
|
351
|
+
const existing = decodeJob(row);
|
|
352
|
+
if (existing.app_id !== source.appId ||
|
|
353
|
+
existing.source_revision !== sourceRevision)
|
|
354
|
+
throw new Error("Build request identity conflict");
|
|
355
|
+
return existing.job_id;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
};
|
|
360
|
+
const existing = reusedRequest();
|
|
361
|
+
if (existing !== null)
|
|
362
|
+
return {
|
|
363
|
+
build: view(required(existing)),
|
|
364
|
+
run: () => Promise.resolve(view(required(existing))),
|
|
365
|
+
};
|
|
366
|
+
if ((await options.computer.port.buildBackend()).kind === "no_backend")
|
|
367
|
+
throw new OysterBuildStartError("no_backend");
|
|
368
|
+
const jobId = `build_${randomHex(16)}`;
|
|
369
|
+
const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
|
|
370
|
+
const uploadGrant = randomHex(32);
|
|
371
|
+
const issuedAt = now();
|
|
372
|
+
const uploadGrantDigest = await sha256Hex(new TextEncoder().encode(uploadGrant));
|
|
373
|
+
const reused = owner.transactionSync(() => {
|
|
374
|
+
const existing = reusedRequest();
|
|
375
|
+
if (existing !== null)
|
|
376
|
+
return existing;
|
|
377
|
+
owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
|
|
378
|
+
recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
|
|
379
|
+
status, artifact_digest, artifact_bytes, detail, installation, request_id, issued_at, expires_at, executable_until)
|
|
380
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, Schema.encodeUnknownSync(locationCodec)(source.location), Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), uploadGrantDigest, input.installAt === undefined
|
|
381
|
+
? null
|
|
382
|
+
: Schema.encodeUnknownSync(installationCodec)({
|
|
383
|
+
kind: "pending",
|
|
384
|
+
installationId: input.installAt,
|
|
385
|
+
}), input.requestId ?? null, issuedAt, issuedAt + jobTtlMs);
|
|
386
|
+
return null;
|
|
387
|
+
});
|
|
388
|
+
if (reused !== null)
|
|
389
|
+
return {
|
|
390
|
+
build: view(required(reused)),
|
|
391
|
+
run: () => Promise.resolve(view(required(reused))),
|
|
392
|
+
};
|
|
393
|
+
// The grant leaves this owner only inside the fixed command.
|
|
394
|
+
const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
|
|
395
|
+
return { build: view(required(jobId)), run: () => run(jobId, command) };
|
|
396
|
+
}
|
|
397
|
+
let installationBusy = false;
|
|
398
|
+
function recordInstallationOutcome(jobId, outcome) {
|
|
399
|
+
return owner.transactionSync(() => {
|
|
400
|
+
const job = readJob(jobId);
|
|
401
|
+
if (job?.status !== "succeeded" ||
|
|
402
|
+
readRelease(jobId) === null ||
|
|
403
|
+
job.installation?.kind !== "pending" ||
|
|
404
|
+
job.installation.installationId !== outcome.installationId)
|
|
405
|
+
return false;
|
|
406
|
+
owner.sql.exec("UPDATE oyster_build_jobs SET installation=? WHERE job_id=?", Schema.encodeUnknownSync(installationCodec)(outcome), jobId);
|
|
407
|
+
return true;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
297
410
|
return Object.freeze({
|
|
298
411
|
/** Record (or replace) an App's build source; the caller is the owner. */
|
|
299
|
-
registerSource
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return
|
|
412
|
+
registerSource,
|
|
413
|
+
/** Resolve repository identity and replace its current recipe as one transition.
|
|
414
|
+
* A fork's existing identity wins over the copied manifest's App id. */
|
|
415
|
+
registerRepositorySource(input) {
|
|
416
|
+
return owner.transactionSync(() => {
|
|
417
|
+
const existing = readRepositorySource(input.location.installationId, input.location.repository);
|
|
418
|
+
const appId = Schema.decodeUnknownSync(appIdSchema)(existing?.appId ?? input.appId);
|
|
419
|
+
if (existing === null && readSource(appId) !== null)
|
|
420
|
+
return { kind: "conflict" };
|
|
421
|
+
const pinned = normalizeAppPublisherRecipe({
|
|
422
|
+
appId,
|
|
423
|
+
recipe: input.recipe,
|
|
424
|
+
});
|
|
425
|
+
return {
|
|
426
|
+
kind: "registered",
|
|
427
|
+
source: registerSource({
|
|
428
|
+
appId,
|
|
429
|
+
location: input.location,
|
|
430
|
+
recipe: pinned.recipe,
|
|
431
|
+
buildRecipeDigest: pinned.buildRecipeDigest,
|
|
432
|
+
}),
|
|
433
|
+
};
|
|
434
|
+
});
|
|
304
435
|
},
|
|
305
436
|
readSource,
|
|
437
|
+
readRepositorySource,
|
|
306
438
|
/**
|
|
307
439
|
* Mint one build job for an App's registered source at one exact commit.
|
|
308
440
|
* A template source builds only a seed revision recorded for its pinned
|
|
@@ -310,33 +442,54 @@ export function createOysterAppBuilds(options) {
|
|
|
310
442
|
* Answers the job and its one run, which the caller keeps alive (the
|
|
311
443
|
* upload grant exists only inside that run's fixed command).
|
|
312
444
|
*/
|
|
313
|
-
|
|
314
|
-
const sourceRevision = Schema.decodeUnknownSync(sourceRevisionSchema)(input.sourceRevision);
|
|
315
|
-
const source = readSource(input.appId);
|
|
316
|
-
if (source === null)
|
|
317
|
-
throw new Error("App has no build source");
|
|
318
|
-
if (source.location.kind === "template") {
|
|
319
|
-
const receipt = await options.templates.read(source.appId, sourceRevision);
|
|
320
|
-
if (receipt?.buildRecipeDigest !== source.buildRecipeDigest)
|
|
321
|
-
throw new Error("Revision is not a seed of this App's template");
|
|
322
|
-
}
|
|
323
|
-
const jobId = `build_${randomHex(16)}`;
|
|
324
|
-
const releaseId = parseReleaseId(`rel_${sourceRevision.slice("git:".length, "git:".length + 12)}${randomHex(4)}`);
|
|
325
|
-
const uploadGrant = randomHex(32);
|
|
326
|
-
const issuedAt = now();
|
|
327
|
-
owner.sql.exec(`INSERT INTO oyster_build_jobs(job_id, app_id, release_id, source_revision, location,
|
|
328
|
-
recipe, recipe_digest, requested_by, credential_id, upload_grant_digest,
|
|
329
|
-
status, artifact_digest, artifact_bytes, detail, issued_at, expires_at, executable_until)
|
|
330
|
-
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, NULL, NULL, ?, ?, 0)`, jobId, source.appId, releaseId, sourceRevision, Schema.encodeUnknownSync(locationCodec)(source.location), Schema.encodeUnknownSync(recipeCodec)(source.recipe), source.buildRecipeDigest, input.requestedBy, randomHex(32), await sha256Hex(new TextEncoder().encode(uploadGrant)), issuedAt, issuedAt + jobTtlMs);
|
|
331
|
-
// The grant leaves this owner only inside the fixed command.
|
|
332
|
-
const command = teamAppBuildCommand({ appId: source.appId, sourceRevision, source }, uploadGrant);
|
|
333
|
-
return { build: view(required(jobId)), run: () => run(jobId, command) };
|
|
334
|
-
},
|
|
445
|
+
start,
|
|
335
446
|
read(jobId) {
|
|
336
447
|
expire();
|
|
337
448
|
const job = readJob(jobId);
|
|
338
449
|
return job === null ? null : view(job);
|
|
339
450
|
},
|
|
451
|
+
/** Discover the current source's latest job from the caller's Git owner,
|
|
452
|
+
* never a job id remembered in a browser or another installation's repo. */
|
|
453
|
+
latestRepositoryBuild(installationId, repository) {
|
|
454
|
+
expire();
|
|
455
|
+
const source = readRepositorySource(installationId, repository);
|
|
456
|
+
if (source === null)
|
|
457
|
+
return null;
|
|
458
|
+
const [row] = owner.sql
|
|
459
|
+
.exec("SELECT * FROM oyster_build_jobs WHERE app_id=? AND json_extract(location, '$.kind')='installation' AND json_extract(location, '$.installationId')=? AND json_extract(location, '$.repository')=? ORDER BY rowid DESC LIMIT 1", source.appId, installationId, repository)
|
|
460
|
+
.toArray();
|
|
461
|
+
return row === undefined ? null : view(decodeJob(row));
|
|
462
|
+
},
|
|
463
|
+
/** One bounded host installation at a time. Only a verified successful
|
|
464
|
+
* build may reach the installer; the same existing row owns its outcome.
|
|
465
|
+
* A Workflow retry may resume a pending decision, never a terminal one. */
|
|
466
|
+
async install(jobId, decide) {
|
|
467
|
+
const job = readJob(jobId);
|
|
468
|
+
const release = readRelease(jobId);
|
|
469
|
+
if (installationBusy ||
|
|
470
|
+
job?.status !== "succeeded" ||
|
|
471
|
+
release === null ||
|
|
472
|
+
job.installation?.kind !== "pending")
|
|
473
|
+
return false;
|
|
474
|
+
installationBusy = true;
|
|
475
|
+
try {
|
|
476
|
+
let outcome;
|
|
477
|
+
try {
|
|
478
|
+
outcome = await decide(release, job.installation.installationId);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
outcome = {
|
|
482
|
+
kind: "failed",
|
|
483
|
+
installationId: job.installation.installationId,
|
|
484
|
+
detail: "Automatic installation unavailable",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return recordInstallationOutcome(jobId, outcome);
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
installationBusy = false;
|
|
491
|
+
}
|
|
492
|
+
},
|
|
340
493
|
/** A built Release by id, for an owner to install or select. */
|
|
341
494
|
release(releaseId) {
|
|
342
495
|
const [row] = owner.sql
|
package/dist/app-storage.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export declare function createAppStorage(options: {
|
|
|
86
86
|
/** Read the declared team layer without exposing any member's private value. */
|
|
87
87
|
read: (key: string) => Effect.Effect<{
|
|
88
88
|
readonly value: Schema.Json;
|
|
89
|
-
readonly source: "
|
|
89
|
+
readonly source: "member" | "team" | "none";
|
|
90
90
|
readonly team: {
|
|
91
91
|
readonly value: Schema.Json;
|
|
92
92
|
readonly revision: number;
|
|
@@ -111,7 +111,7 @@ export declare function createAppStorage(options: {
|
|
|
111
111
|
/** Resolve declared member and team layers under current live admission. */
|
|
112
112
|
read: (key: string) => Effect.Effect<{
|
|
113
113
|
readonly value: Schema.Json;
|
|
114
|
-
readonly source: "
|
|
114
|
+
readonly source: "member" | "team" | "none";
|
|
115
115
|
readonly team: {
|
|
116
116
|
readonly value: Schema.Json;
|
|
117
117
|
readonly revision: number;
|
|
@@ -134,7 +134,7 @@ export declare function createAppStorage(options: {
|
|
|
134
134
|
events: (key: string) => Stream.Stream<{
|
|
135
135
|
readonly appId: string;
|
|
136
136
|
readonly key: string;
|
|
137
|
-
readonly scope: "
|
|
137
|
+
readonly scope: "member" | "team";
|
|
138
138
|
readonly revision: number;
|
|
139
139
|
} | null, AppStorageError, never>;
|
|
140
140
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Approvals } from "@fungi.computer/approvals";
|
|
2
|
+
import type { HostActor } from "@fungi.computer/puffball/app";
|
|
3
|
+
import type { OysterCapability } from "./host.js";
|
|
4
|
+
/** Ordinary capability routes; the outer App gate supplies identity and consent. */
|
|
5
|
+
export declare function approvalCapability(control: (actor: HostActor) => Approvals): OysterCapability;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { bodyLimit } from "hono/body-limit";
|
|
3
|
+
import { causeClass } from "./cause-class.js";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import { ApprovalListInputSchema, ApprovalGetInputSchema, ApprovalAnswerInputSchema, AutoApproveInputSchema, } from "@fungi.computer/approvals";
|
|
6
|
+
/** Ordinary capability routes; the outer App gate supplies identity and consent. */
|
|
7
|
+
export function approvalCapability(control) {
|
|
8
|
+
const routes = new Hono();
|
|
9
|
+
routes.use("*", bodyLimit({ maxSize: 4096 }));
|
|
10
|
+
routes.use("*", async (context, next) => {
|
|
11
|
+
if (context.env.principal.actor.kind !== "member")
|
|
12
|
+
return context.json({ kind: "refused", reason: "not-eligible" }, 403);
|
|
13
|
+
await next();
|
|
14
|
+
return undefined;
|
|
15
|
+
});
|
|
16
|
+
routes.onError((cause, context) => {
|
|
17
|
+
if (cause instanceof SyntaxError || cause instanceof Schema.SchemaError)
|
|
18
|
+
return context.json({ error: "Invalid approval request" }, 400);
|
|
19
|
+
console.warn("Native approval control failed", {
|
|
20
|
+
causeClass: causeClass(cause),
|
|
21
|
+
});
|
|
22
|
+
return context.json({ error: "Approval home unavailable" }, 503);
|
|
23
|
+
});
|
|
24
|
+
const strict = { onExcessProperty: "error" };
|
|
25
|
+
routes.post("/list", async (c) => c.json(await control(c.env.principal.actor).list(Schema.decodeUnknownSync(ApprovalListInputSchema, strict)(await c.req.json()))));
|
|
26
|
+
routes.post("/get", async (c) => c.json(await control(c.env.principal.actor).get(Schema.decodeUnknownSync(ApprovalGetInputSchema, strict)(await c.req.json()))));
|
|
27
|
+
routes.post("/answer", async (c) => c.json(await control(c.env.principal.actor).answer(Schema.decodeUnknownSync(ApprovalAnswerInputSchema, strict)(await c.req.json()))));
|
|
28
|
+
routes.post("/set-auto-approve", async (c) => c.json(await control(c.env.principal.actor).setAutoApprove(Schema.decodeUnknownSync(AutoApproveInputSchema, strict)(await c.req.json()))));
|
|
29
|
+
return {
|
|
30
|
+
id: "approvals",
|
|
31
|
+
version: 1,
|
|
32
|
+
permission: "approvals",
|
|
33
|
+
base: "/approvals/",
|
|
34
|
+
routes,
|
|
35
|
+
methods: {
|
|
36
|
+
list: { kind: "json", method: "POST", path: "list" },
|
|
37
|
+
get: { kind: "json", method: "POST", path: "get" },
|
|
38
|
+
answer: { kind: "json", method: "POST", path: "answer" },
|
|
39
|
+
setAutoApprove: {
|
|
40
|
+
kind: "json",
|
|
41
|
+
method: "POST",
|
|
42
|
+
path: "set-auto-approve",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ApprovalLedger, type Approvals, type PendingApproval, type ApprovalApplyResult } from "@fungi.computer/approvals";
|
|
3
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
4
|
+
import { type OysterHost } from "./host.js";
|
|
5
|
+
import type { HostActor } from "@fungi.computer/puffball/app";
|
|
6
|
+
import type { TenantScope } from "./tenant.js";
|
|
7
|
+
declare const callSchema: z.ZodObject<{
|
|
8
|
+
actor: import("zod/mini").ZodMiniDiscriminatedUnion<[import("zod/mini").ZodMiniObject<{
|
|
9
|
+
kind: import("zod/mini").ZodMiniLiteral<"member">;
|
|
10
|
+
memberId: import("zod/mini").ZodMiniString<string>;
|
|
11
|
+
}, z.core.$strip>, import("zod/mini").ZodMiniObject<{
|
|
12
|
+
kind: import("zod/mini").ZodMiniLiteral<"agent">;
|
|
13
|
+
agentId: import("zod/mini").ZodMiniString<string>;
|
|
14
|
+
}, z.core.$strip>, import("zod/mini").ZodMiniObject<{
|
|
15
|
+
kind: import("zod/mini").ZodMiniLiteral<"team-key">;
|
|
16
|
+
keyId: import("zod/mini").ZodMiniString<string>;
|
|
17
|
+
}, z.core.$strip>], "kind">;
|
|
18
|
+
installationId: z.ZodString;
|
|
19
|
+
sessionId: z.ZodNullable<z.ZodString>;
|
|
20
|
+
operation: z.ZodString;
|
|
21
|
+
access: z.ZodEnum<{
|
|
22
|
+
write: "write";
|
|
23
|
+
external: "external";
|
|
24
|
+
}>;
|
|
25
|
+
authorityFence: z.ZodString;
|
|
26
|
+
idempotencyKey: z.ZodString;
|
|
27
|
+
input: z.ZodJSONSchema;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
/** Host-generated App call metadata; the operation's input contains no actor or approval choice. */
|
|
30
|
+
export type OysterApprovalCall = z.output<typeof callSchema>;
|
|
31
|
+
/** The registry's durable App effect home. Mycelium still owns operation/schema invocation. */
|
|
32
|
+
export declare class OysterApprovalHome {
|
|
33
|
+
private readonly host;
|
|
34
|
+
readonly ledger: ApprovalLedger;
|
|
35
|
+
constructor(host: Readonly<{
|
|
36
|
+
owner: WatchdogSqliteOwner;
|
|
37
|
+
scope: TenantScope;
|
|
38
|
+
current: () => Promise<OysterHost>;
|
|
39
|
+
/** Commit only intent state and its wake, never an App effect. */
|
|
40
|
+
commit: <T>(write: () => Promise<T>, nextWakeAt: () => number | null) => Promise<T>;
|
|
41
|
+
}>);
|
|
42
|
+
private createLedger;
|
|
43
|
+
submit(raw: OysterApprovalCall): Promise<PendingApproval | ApprovalApplyResult>;
|
|
44
|
+
/** Bind each read/decision to current membership, not the renderer's actor bytes. */
|
|
45
|
+
capability(rawActor: HostActor): Approvals;
|
|
46
|
+
/** One native alarm quantum; apply owns the fresh grant/fence check and single claim. */
|
|
47
|
+
reconcile(): Promise<number | null>;
|
|
48
|
+
private resolve;
|
|
49
|
+
private execution;
|
|
50
|
+
}
|
|
51
|
+
export {};
|