@relayflows/sdk 2.0.28 → 2.0.30
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/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +10 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-step-index.d.ts +6 -0
- package/dist/authored-step-index.d.ts.map +1 -1
- package/dist/authored-step-index.js +42 -14
- package/dist/authored-step-index.js.map +1 -1
- package/dist/bundle-extensions.d.ts.map +1 -1
- package/dist/bundle-extensions.js +4 -7
- package/dist/bundle-extensions.js.map +1 -1
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +41 -7
- package/dist/bundle.js.map +1 -1
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +18 -6
- package/dist/canonical.js.map +1 -1
- package/dist/cli/status.d.ts +12 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +39 -1
- package/dist/cli/status.js.map +1 -1
- package/dist/flow-extension-compat.d.ts.map +1 -1
- package/dist/flow-extension-compat.js +26 -8
- package/dist/flow-extension-compat.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +7 -0
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +48 -22
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/fs-descriptor.d.ts +17 -0
- package/dist/fs-descriptor.d.ts.map +1 -0
- package/dist/fs-descriptor.js +96 -0
- package/dist/fs-descriptor.js.map +1 -0
- package/dist/hosted-base-snapshot.d.ts +25 -0
- package/dist/hosted-base-snapshot.d.ts.map +1 -0
- package/dist/hosted-base-snapshot.js +278 -0
- package/dist/hosted-base-snapshot.js.map +1 -0
- package/dist/hosted-extension-declarations.d.ts +10 -0
- package/dist/hosted-extension-declarations.d.ts.map +1 -0
- package/dist/hosted-extension-declarations.js +272 -0
- package/dist/hosted-extension-declarations.js.map +1 -0
- package/dist/hosted-extension-isolation.d.ts +82 -0
- package/dist/hosted-extension-isolation.d.ts.map +1 -0
- package/dist/hosted-extension-isolation.js +322 -0
- package/dist/hosted-extension-isolation.js.map +1 -0
- package/dist/hosted-extension-manifest.d.ts +9 -0
- package/dist/hosted-extension-manifest.d.ts.map +1 -0
- package/dist/hosted-extension-manifest.js +235 -0
- package/dist/hosted-extension-manifest.js.map +1 -0
- package/dist/hosted-extension-protocol.d.ts +16 -0
- package/dist/hosted-extension-protocol.d.ts.map +1 -0
- package/dist/hosted-extension-protocol.js +224 -0
- package/dist/hosted-extension-protocol.js.map +1 -0
- package/dist/hosted-extension-runtime.d.ts +30 -0
- package/dist/hosted-extension-runtime.d.ts.map +1 -0
- package/dist/hosted-extension-runtime.js +167 -0
- package/dist/hosted-extension-runtime.js.map +1 -0
- package/dist/hosted-extension-sandbox-source.d.ts +9 -0
- package/dist/hosted-extension-sandbox-source.d.ts.map +1 -0
- package/dist/hosted-extension-sandbox-source.js +95 -0
- package/dist/hosted-extension-sandbox-source.js.map +1 -0
- package/dist/hosted-extension-sandbox.d.ts +39 -0
- package/dist/hosted-extension-sandbox.d.ts.map +1 -0
- package/dist/hosted-extension-sandbox.js +418 -0
- package/dist/hosted-extension-sandbox.js.map +1 -0
- package/dist/hosted-project.d.ts +3 -0
- package/dist/hosted-project.d.ts.map +1 -0
- package/dist/hosted-project.js +19 -0
- package/dist/hosted-project.js.map +1 -0
- package/dist/hosted-promise-safety.d.ts +12 -0
- package/dist/hosted-promise-safety.d.ts.map +1 -0
- package/dist/hosted-promise-safety.js +63 -0
- package/dist/hosted-promise-safety.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/intrinsic-array.d.ts +3 -0
- package/dist/intrinsic-array.d.ts.map +1 -0
- package/dist/intrinsic-array.js +14 -0
- package/dist/intrinsic-array.js.map +1 -0
- package/dist/json-value.d.ts +6 -1
- package/dist/json-value.d.ts.map +1 -1
- package/dist/json-value.js +155 -36
- package/dist/json-value.js.map +1 -1
- package/dist/llm-worker.d.ts +9 -0
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +15 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/plugin-store.d.ts +8 -2
- package/dist/plugin-store.d.ts.map +1 -1
- package/dist/plugin-store.js +233 -56
- package/dist/plugin-store.js.map +1 -1
- package/dist/semver-range.d.ts.map +1 -1
- package/dist/semver-range.js +27 -15
- package/dist/semver-range.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-flow-executor.ts +10 -1
- package/src/authored-step-index.ts +46 -12
- package/src/bundle-extensions.ts +6 -5
- package/src/bundle.ts +52 -7
- package/src/canonical.ts +20 -6
- package/src/cli/status.ts +50 -1
- package/src/flow-extension-compat.ts +32 -7
- package/src/flow-extension-loader.ts +56 -21
- package/src/fs-descriptor.ts +127 -0
- package/src/hosted-base-snapshot.ts +351 -0
- package/src/hosted-extension-declarations.ts +387 -0
- package/src/hosted-extension-isolation.ts +457 -0
- package/src/hosted-extension-manifest.ts +249 -0
- package/src/hosted-extension-protocol.ts +262 -0
- package/src/hosted-extension-runtime.ts +266 -0
- package/src/hosted-extension-sandbox-source.ts +94 -0
- package/src/hosted-extension-sandbox.ts +489 -0
- package/src/hosted-project.ts +18 -0
- package/src/hosted-promise-safety.ts +68 -0
- package/src/index.ts +18 -0
- package/src/intrinsic-array.ts +14 -0
- package/src/json-value.ts +184 -32
- package/src/llm-worker.ts +15 -1
- package/src/plugin-store.ts +323 -56
- package/src/semver-range.ts +39 -14
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { canonicalize } from './canonical.js';
|
|
2
|
+
import { sha256 } from './bundle.js';
|
|
3
|
+
import type { FlowExtensionManifest } from './flow-extension-manifest.js';
|
|
4
|
+
import { validateHostedFlowExtensionManifest } from './hosted-extension-manifest.js';
|
|
5
|
+
import { assertBaseCompatible, assertCompatible, runtimeVersions } from './flow-extension-compat.js';
|
|
6
|
+
import {
|
|
7
|
+
hostedExtensionDispatchIdentity,
|
|
8
|
+
type HostedExtensionDispatch,
|
|
9
|
+
} from './flow-extension-loader.js';
|
|
10
|
+
import {
|
|
11
|
+
assertHostedInstallationAuthority,
|
|
12
|
+
assertHostedRuntimeAuthority,
|
|
13
|
+
type HostedExtensionArtifact,
|
|
14
|
+
type HostedExtensionBase,
|
|
15
|
+
type HostedExtensionInstallation,
|
|
16
|
+
} from './hosted-extension-runtime.js';
|
|
17
|
+
import { PluginError } from './plugin-manifest.js';
|
|
18
|
+
import {
|
|
19
|
+
assertHostedPromiseSafety,
|
|
20
|
+
frozenHostedPromiseValue,
|
|
21
|
+
} from './hosted-promise-safety.js';
|
|
22
|
+
import { appendIntrinsicArray } from './intrinsic-array.js';
|
|
23
|
+
import { readStoredPluginFiles } from './plugin-store.js';
|
|
24
|
+
import {
|
|
25
|
+
boundedJsonSnapshot,
|
|
26
|
+
type HostedExtensionProtocolResult,
|
|
27
|
+
} from './hosted-extension-protocol.js';
|
|
28
|
+
import { runHostedExtensionSandbox } from './hosted-extension-sandbox.js';
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
loadHostedExtensionArtifacts,
|
|
32
|
+
loadHostedExtensionBase,
|
|
33
|
+
loadHostedExtensionRuntime,
|
|
34
|
+
} from './hosted-extension-runtime.js';
|
|
35
|
+
export type {
|
|
36
|
+
HostedExtensionArtifact,
|
|
37
|
+
HostedExtensionBase,
|
|
38
|
+
HostedExtensionInstallation,
|
|
39
|
+
HostedExtensionRuntime,
|
|
40
|
+
} from './hosted-extension-runtime.js';
|
|
41
|
+
|
|
42
|
+
const HOSTED_WRITE = 'cloud:babysitter-turn';
|
|
43
|
+
const ARRAY_IS_ARRAY = Array.isArray;
|
|
44
|
+
const BUFFER_TO_STRING = Function.prototype.call.bind(Buffer.prototype.toString) as (
|
|
45
|
+
value: Buffer,
|
|
46
|
+
encoding: BufferEncoding,
|
|
47
|
+
) => string;
|
|
48
|
+
const JSON_PARSE = JSON.parse;
|
|
49
|
+
const NUMBER_IS_SAFE_INTEGER = Number.isSafeInteger;
|
|
50
|
+
const OBJECT_HAS_OWN = Object.hasOwn;
|
|
51
|
+
const OBJECT_FREEZE = Object.freeze;
|
|
52
|
+
const OBJECT_GET_OWN_PROPERTY_DESCRIPTOR = Object.getOwnPropertyDescriptor;
|
|
53
|
+
const OBJECT_KEYS = Object.keys;
|
|
54
|
+
const REGEXP_TEST = Function.prototype.call.bind(RegExp.prototype.test) as (
|
|
55
|
+
regexp: RegExp, value: string,
|
|
56
|
+
) => boolean;
|
|
57
|
+
const STRING_STARTS_WITH = Function.prototype.call.bind(String.prototype.startsWith) as (
|
|
58
|
+
value: string, search: string,
|
|
59
|
+
) => boolean;
|
|
60
|
+
const BABYSITTER_REF = 'github:AgentWorkforce/flows@8b33ebab8347514f80d9da5a81206a087f641714#extensions/babysitter';
|
|
61
|
+
const BABYSITTER_DIGEST = 'bdf2187b9a242667d34bbc63e7a744753e146dc8cd6f4047047f2aed28f406ee';
|
|
62
|
+
const BABYSITTER_MANIFEST_SHA256 = '5631a06bbdc8186f4ee0ff955610ead24d001c5197b59fb1fe81fe422c44f226';
|
|
63
|
+
const DELIVERY_ID = /^[A-Za-z0-9_.:-]{1,200}$/;
|
|
64
|
+
const RECEIPT_ID = /^[A-Za-z0-9_.:-]{1,200}$/;
|
|
65
|
+
const OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
66
|
+
const REPOSITORY = /^[A-Za-z0-9._-]{1,100}$/;
|
|
67
|
+
const HEAD_SHA = /^[a-f0-9]{40}$/;
|
|
68
|
+
const NATIVE_TRIGGERS = [
|
|
69
|
+
{ provider: 'github', event: 'pull_request', actions: ['opened', 'synchronize', 'reopened', 'ready_for_review', 'closed', 'labeled', 'unlabeled'] },
|
|
70
|
+
{ provider: 'github', event: 'pull_request_review', actions: ['submitted', 'dismissed'] },
|
|
71
|
+
{ provider: 'github', event: 'check_run', actions: ['completed'] },
|
|
72
|
+
{ provider: 'github', event: 'issue_comment', actions: ['created'] },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export interface HostedCapabilityAuthority {
|
|
76
|
+
readonly dispatch: HostedExtensionDispatch;
|
|
77
|
+
readonly extension: Readonly<{
|
|
78
|
+
name: string;
|
|
79
|
+
version: string;
|
|
80
|
+
ref: string;
|
|
81
|
+
digest: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface HostedBabysitterCapability {
|
|
86
|
+
queue(request: unknown, authority: HostedCapabilityAuthority): Promise<unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RunHostedExtensionOptions {
|
|
90
|
+
/** Complete, lock-ordered set returned by loadHostedExtensionRuntime. */
|
|
91
|
+
readonly installation: HostedExtensionInstallation;
|
|
92
|
+
/** Same-generation base returned by loadHostedExtensionRuntime. */
|
|
93
|
+
readonly base: HostedExtensionBase;
|
|
94
|
+
readonly dispatch: HostedExtensionDispatch;
|
|
95
|
+
/** Host-normalized delivery descriptor. Its event identity must equal dispatch. */
|
|
96
|
+
readonly input: unknown;
|
|
97
|
+
readonly babysitterTurn: HostedBabysitterCapability;
|
|
98
|
+
readonly timeoutMs?: number;
|
|
99
|
+
/** Test/packaging override. Production resolves /usr/bin/bwrap. */
|
|
100
|
+
readonly bubblewrapPath?: string;
|
|
101
|
+
/** Test/packaging override. Defaults to the current, fingerprinted Node executable. */
|
|
102
|
+
readonly nodePath?: string;
|
|
103
|
+
/** Test/packaging override. Production resolves /usr/bin/prlimit. */
|
|
104
|
+
readonly prlimitPath?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type HostedExtensionResult = HostedExtensionProtocolResult;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Execute a capability-only hosted extension in a Linux mount/PID/network/user
|
|
111
|
+
* namespace. The extension is first imported inside that namespace. It sees
|
|
112
|
+
* no host workspace, environment credentials, network, child process, MCP,
|
|
113
|
+
* helpers, harnesses, or base-flow context. The parent exposes exactly one
|
|
114
|
+
* authenticated capability adapter and passes the original branded dispatch
|
|
115
|
+
* authority to that adapter out of band.
|
|
116
|
+
*
|
|
117
|
+
* This is deliberately not wired into executeAuthoredFlow yet. #549's refusal
|
|
118
|
+
* remains the rollout gate until the Cloud adapter and independent review land.
|
|
119
|
+
*/
|
|
120
|
+
export async function runHostedCapabilityExtension(
|
|
121
|
+
options: RunHostedExtensionOptions,
|
|
122
|
+
): Promise<HostedExtensionResult> {
|
|
123
|
+
assertHostedPromiseSafety('plugin_unsupported');
|
|
124
|
+
const identity = hostedExtensionDispatchIdentity(options.dispatch);
|
|
125
|
+
await assertHostedRuntimeAuthority(options.installation, options.base);
|
|
126
|
+
const { artifact, manifest } = await selectHostedExtensionForRuntime(
|
|
127
|
+
options.installation,
|
|
128
|
+
options.base,
|
|
129
|
+
identity,
|
|
130
|
+
runtimeVersions(),
|
|
131
|
+
);
|
|
132
|
+
return await runVerifiedNativeExtensionSandbox({
|
|
133
|
+
...options,
|
|
134
|
+
artifact,
|
|
135
|
+
manifest,
|
|
136
|
+
beforeLaunch: () => assertHostedRuntimeAuthority(options.installation, options.base),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface RunVerifiedNativeExtensionOptions {
|
|
141
|
+
readonly artifact: HostedExtensionArtifact;
|
|
142
|
+
readonly manifest: FlowExtensionManifest;
|
|
143
|
+
readonly dispatch: HostedExtensionDispatch;
|
|
144
|
+
readonly input: unknown;
|
|
145
|
+
readonly babysitterTurn: HostedBabysitterCapability;
|
|
146
|
+
readonly timeoutMs?: number;
|
|
147
|
+
readonly bubblewrapPath?: string;
|
|
148
|
+
readonly nodePath?: string;
|
|
149
|
+
readonly prlimitPath?: string;
|
|
150
|
+
readonly surfaceRoot?: string;
|
|
151
|
+
readonly beforeLaunch?: () => Promise<void>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @internal Security-harness seam; not exported from the SDK package root. */
|
|
155
|
+
export async function runVerifiedNativeExtensionSandbox(
|
|
156
|
+
options: RunVerifiedNativeExtensionOptions,
|
|
157
|
+
): Promise<HostedExtensionResult> {
|
|
158
|
+
assertHostedPromiseSafety('plugin_unsupported');
|
|
159
|
+
assertCapabilityOnlyManifest(options.manifest);
|
|
160
|
+
const identity = hostedExtensionDispatchIdentity(options.dispatch);
|
|
161
|
+
assertManifestRoutes(options.manifest, identity);
|
|
162
|
+
const normalizedInput = babysitterInput(options.input, options.dispatch);
|
|
163
|
+
const versions = runtimeVersions();
|
|
164
|
+
const authority: HostedCapabilityAuthority = OBJECT_FREEZE({
|
|
165
|
+
dispatch: options.dispatch,
|
|
166
|
+
extension: OBJECT_FREEZE({
|
|
167
|
+
name: options.manifest.name,
|
|
168
|
+
version: options.manifest.version,
|
|
169
|
+
ref: options.artifact.ref,
|
|
170
|
+
digest: options.artifact.digest,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
return await runHostedExtensionSandbox({
|
|
174
|
+
artifactDirectory: options.artifact.directory,
|
|
175
|
+
artifactDigest: options.artifact.digest,
|
|
176
|
+
entry: options.manifest.entry,
|
|
177
|
+
surfaceVersion: versions.surface,
|
|
178
|
+
identity,
|
|
179
|
+
input: normalizedInput,
|
|
180
|
+
timeoutMs: ownOption<number>(options, 'timeoutMs'),
|
|
181
|
+
bubblewrapPath: ownOption<string>(options, 'bubblewrapPath'),
|
|
182
|
+
nodePath: ownOption<string>(options, 'nodePath'),
|
|
183
|
+
prlimitPath: ownOption<string>(options, 'prlimitPath'),
|
|
184
|
+
surfaceRoot: ownOption<string>(options, 'surfaceRoot'),
|
|
185
|
+
beforeLaunch: ownOption<() => Promise<void>>(options, 'beforeLaunch'),
|
|
186
|
+
invoke: async request => babysitterReceipt(await options.babysitterTurn.queue(
|
|
187
|
+
babysitterRequest(request, normalizedInput, options.dispatch),
|
|
188
|
+
authority,
|
|
189
|
+
)),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @internal Selection seam used by compatibility/security regression tests. */
|
|
194
|
+
export async function selectHostedExtensionForRuntime(
|
|
195
|
+
value: HostedExtensionInstallation,
|
|
196
|
+
base: HostedExtensionBase,
|
|
197
|
+
identity: { readonly provider: string; readonly event: string; readonly action?: string },
|
|
198
|
+
versions: Readonly<{ sdk: string; surface: string }>,
|
|
199
|
+
): Promise<{ artifact: HostedExtensionArtifact; manifest: FlowExtensionManifest }> {
|
|
200
|
+
assertHostedPromiseSafety('plugin_source_invalid');
|
|
201
|
+
assertHostedInstallationAuthority(value);
|
|
202
|
+
if (typeof base !== 'object' || base === null || typeof base.name !== 'string'
|
|
203
|
+
|| (base.version !== undefined && typeof base.version !== 'string')) {
|
|
204
|
+
throw new PluginError('plugin_incompatible', 'Hosted base identity is malformed.');
|
|
205
|
+
}
|
|
206
|
+
const matches: Array<{ artifact: HostedExtensionArtifact; manifest: FlowExtensionManifest }> = [];
|
|
207
|
+
for (let index = 0; index < value.artifacts.length; index += 1) {
|
|
208
|
+
const artifact = value.artifacts[index]!;
|
|
209
|
+
const manifest = await verifiedManifest(artifact);
|
|
210
|
+
assertCompatible(manifest, versions);
|
|
211
|
+
assertBaseCompatible(manifest, base);
|
|
212
|
+
if (hostedManifestRoutes(manifest, identity)) appendIntrinsicArray(matches, { artifact, manifest });
|
|
213
|
+
}
|
|
214
|
+
if (matches.length > 1) {
|
|
215
|
+
throw new PluginError(
|
|
216
|
+
'plugin_event_ambiguous',
|
|
217
|
+
`Hosted event matches multiple extension manifests (${manifestNames(matches)}).`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const selected = matches[0];
|
|
221
|
+
if (selected === undefined || selected.artifact.ref !== BABYSITTER_REF) {
|
|
222
|
+
throw new PluginError('plugin_event_unroutable', 'Hosted event does not route to the pinned native Babysitter extension.');
|
|
223
|
+
}
|
|
224
|
+
await assertPinnedBabysitter(selected.artifact);
|
|
225
|
+
assertCapabilityOnlyManifest(selected.manifest);
|
|
226
|
+
return frozenHostedPromiseValue(selected);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function verifiedManifest(artifact: HostedExtensionArtifact): Promise<FlowExtensionManifest> {
|
|
230
|
+
if (!matches(/^[a-f0-9]{64}$/, artifact.digest) || !matches(/^[a-f0-9]{64}$/, artifact.manifestSha256)) {
|
|
231
|
+
throw new PluginError('plugin_source_drift', `${artifact.ref}: hosted extension digests are malformed.`);
|
|
232
|
+
}
|
|
233
|
+
const stored = await readStoredPluginFiles(artifact.directory, artifact.digest);
|
|
234
|
+
let bytes: Buffer | undefined;
|
|
235
|
+
for (let index = 0; index < stored.length; index += 1) {
|
|
236
|
+
if (stored[index]!.path === 'flows-plugin.json') bytes = stored[index]!.data;
|
|
237
|
+
}
|
|
238
|
+
if (bytes === undefined) {
|
|
239
|
+
throw new PluginError('plugin_source_drift', `${artifact.ref}: flows-plugin.json is missing.`);
|
|
240
|
+
}
|
|
241
|
+
if (sha256(bytes) !== artifact.manifestSha256) {
|
|
242
|
+
throw new PluginError('plugin_source_drift', `${artifact.ref}: flows-plugin.json differs from the lockfile's manifest hash.`);
|
|
243
|
+
}
|
|
244
|
+
let input: unknown;
|
|
245
|
+
try { input = JSON_PARSE(BUFFER_TO_STRING(bytes, 'utf8')); }
|
|
246
|
+
catch { throw new PluginError('plugin_manifest_invalid', `${artifact.ref}: flows-plugin.json is not valid JSON.`); }
|
|
247
|
+
const manifest = validateHostedFlowExtensionManifest(input);
|
|
248
|
+
if (manifest.name !== artifact.name || manifest.version !== artifact.version) {
|
|
249
|
+
throw new PluginError(
|
|
250
|
+
'plugin_source_drift',
|
|
251
|
+
`${artifact.ref}: manifest names ${manifest.name}@${manifest.version}, lockfile has ${artifact.name}@${artifact.version}.`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return manifest;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function assertPinnedBabysitter(artifact: HostedExtensionArtifact): Promise<void> {
|
|
258
|
+
if (artifact.ref !== BABYSITTER_REF || artifact.name !== 'babysitter' || artifact.version !== '0.2.0'
|
|
259
|
+
|| artifact.digest !== BABYSITTER_DIGEST || artifact.manifestSha256 !== BABYSITTER_MANIFEST_SHA256) {
|
|
260
|
+
throw new PluginError(
|
|
261
|
+
'plugin_source_invalid',
|
|
262
|
+
'Hosted capability isolation accepts only the reviewed native Babysitter artifact.',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const stored = await readStoredPluginFiles(artifact.directory, artifact.digest);
|
|
266
|
+
for (let index = 0; index < stored.length; index += 1) {
|
|
267
|
+
const path = stored[index]!.path;
|
|
268
|
+
if (path === 'node_modules' || STRING_STARTS_WITH(path, 'node_modules/')) {
|
|
269
|
+
throw new PluginError('plugin_source_drift', `${artifact.ref}: hosted extensions cannot carry node_modules.`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function assertCapabilityOnlyManifest(manifest: FlowExtensionManifest): void {
|
|
275
|
+
const expected = {
|
|
276
|
+
integrations: ['github'], harnesses: ['codex'], mcp: [], writes: [HOSTED_WRITE],
|
|
277
|
+
base: [{ name: 'software-factory', version: '*' }],
|
|
278
|
+
budget: { dollars: 1, wallclock: '5m' },
|
|
279
|
+
triggers: NATIVE_TRIGGERS,
|
|
280
|
+
};
|
|
281
|
+
const actual = {
|
|
282
|
+
integrations: manifest.permissions.integrations,
|
|
283
|
+
harnesses: manifest.permissions.harnesses,
|
|
284
|
+
mcp: manifest.permissions.mcp,
|
|
285
|
+
writes: manifest.permissions.writes,
|
|
286
|
+
base: manifest.compat.base,
|
|
287
|
+
budget: manifest.permissions.budget,
|
|
288
|
+
triggers: manifest.triggers,
|
|
289
|
+
};
|
|
290
|
+
if (canonicalize(actual) !== canonicalize(expected)
|
|
291
|
+
|| manifest.name !== 'babysitter'
|
|
292
|
+
|| manifest.entry !== 'babysitter.flow.ts'
|
|
293
|
+
|| !manifest.extends.handlers || manifest.extends.hooks.length !== 0
|
|
294
|
+
|| manifest.preflight.credentials.length !== 0 || manifest.preflight.servers.length !== 0) {
|
|
295
|
+
throw new PluginError(
|
|
296
|
+
'plugin_unsupported',
|
|
297
|
+
`${manifest.name}: hosted capability isolation accepts only the native Babysitter permission profile.`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function assertManifestRoutes(
|
|
303
|
+
manifest: FlowExtensionManifest,
|
|
304
|
+
identity: { readonly provider: string; readonly event: string; readonly action?: string },
|
|
305
|
+
): void {
|
|
306
|
+
if (!hostedManifestRoutes(manifest, identity)) {
|
|
307
|
+
throw new PluginError('plugin_event_unroutable', `${manifest.name}: hosted event is not declared exactly once.`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Generic handlers overlap action-specific deliveries, matching the SDK handler router. */
|
|
312
|
+
export function hostedManifestRoutes(
|
|
313
|
+
manifest: Pick<FlowExtensionManifest, 'triggers'>,
|
|
314
|
+
identity: { readonly provider: string; readonly event: string; readonly action?: string },
|
|
315
|
+
): boolean {
|
|
316
|
+
for (let triggerIndex = 0; triggerIndex < manifest.triggers.length; triggerIndex += 1) {
|
|
317
|
+
const trigger = manifest.triggers[triggerIndex]!;
|
|
318
|
+
if (trigger.provider !== identity.provider || trigger.event !== identity.event) continue;
|
|
319
|
+
if (trigger.actions.length === 0) return true;
|
|
320
|
+
if (identity.action === undefined) continue;
|
|
321
|
+
for (let actionIndex = 0; actionIndex < trigger.actions.length; actionIndex += 1) {
|
|
322
|
+
if (trigger.actions[actionIndex] === identity.action) return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function babysitterInput(input: unknown, dispatch: HostedExtensionDispatch): unknown {
|
|
329
|
+
const snapshot = boundedJsonSnapshot(input, 'hosted extension input');
|
|
330
|
+
const top = exactRecord(snapshot, 'Hosted extension input', ['event', 'pullRequest']);
|
|
331
|
+
const event = exactRecord(top.event, 'Hosted extension event', ['provider', 'eventType', 'deliveryId']);
|
|
332
|
+
const pullRequest = optionalRecord(
|
|
333
|
+
top.pullRequest,
|
|
334
|
+
'Hosted extension pull request',
|
|
335
|
+
['host', 'owner', 'repo', 'number', 'headSha'],
|
|
336
|
+
['owner', 'repo', 'number'],
|
|
337
|
+
);
|
|
338
|
+
if (event.provider !== dispatch.provider || event.eventType !== dispatch.eventType
|
|
339
|
+
|| event.deliveryId !== dispatch.deliveryId || event.provider !== 'github'
|
|
340
|
+
|| typeof event.deliveryId !== 'string' || !matches(DELIVERY_ID, event.deliveryId)
|
|
341
|
+
|| (pullRequest.host !== undefined && pullRequest.host !== 'github')
|
|
342
|
+
|| typeof pullRequest.owner !== 'string' || !matches(OWNER, pullRequest.owner)
|
|
343
|
+
|| typeof pullRequest.repo !== 'string' || !matches(REPOSITORY, pullRequest.repo)
|
|
344
|
+
|| pullRequest.repo === '.' || pullRequest.repo === '..'
|
|
345
|
+
|| typeof pullRequest.number !== 'number' || !NUMBER_IS_SAFE_INTEGER(pullRequest.number) || pullRequest.number <= 0
|
|
346
|
+
|| (pullRequest.headSha !== undefined
|
|
347
|
+
&& (typeof pullRequest.headSha !== 'string' || !matches(HEAD_SHA, pullRequest.headSha)))) {
|
|
348
|
+
throw new PluginError('plugin_event_unroutable', 'Hosted extension input does not match the verified GitHub delivery.');
|
|
349
|
+
}
|
|
350
|
+
return snapshot;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function babysitterRequest(value: unknown, input: unknown, dispatch: HostedExtensionDispatch): unknown {
|
|
354
|
+
const snapshot = boundedJsonSnapshot(value, 'Babysitter capability request');
|
|
355
|
+
const request = exactRecord(snapshot, 'Babysitter capability request', ['delivery']);
|
|
356
|
+
const delivery = exactRecord(request.delivery, 'Babysitter delivery', ['deliveryId', 'provider', 'eventType', 'pullRequest']);
|
|
357
|
+
const pullRequest = exactRecord(delivery.pullRequest, 'Babysitter pull request', ['owner', 'repository', 'number']);
|
|
358
|
+
const normalized = exactRecord(input, 'Hosted extension input', ['event', 'pullRequest']);
|
|
359
|
+
const inputPullRequest = record(normalized.pullRequest, 'Hosted extension pull request');
|
|
360
|
+
if (delivery.deliveryId !== dispatch.deliveryId
|
|
361
|
+
|| typeof delivery.deliveryId !== 'string' || !matches(DELIVERY_ID, delivery.deliveryId)
|
|
362
|
+
|| delivery.provider !== 'github' || delivery.eventType !== dispatch.eventType
|
|
363
|
+
|| typeof pullRequest.owner !== 'string' || !matches(OWNER, pullRequest.owner)
|
|
364
|
+
|| typeof pullRequest.repository !== 'string' || !matches(REPOSITORY, pullRequest.repository)
|
|
365
|
+
|| pullRequest.repository === '.' || pullRequest.repository === '..'
|
|
366
|
+
|| typeof pullRequest.number !== 'number' || !NUMBER_IS_SAFE_INTEGER(pullRequest.number) || pullRequest.number <= 0
|
|
367
|
+
|| pullRequest.owner !== inputPullRequest.owner
|
|
368
|
+
|| pullRequest.repository !== inputPullRequest.repo
|
|
369
|
+
|| pullRequest.number !== inputPullRequest.number) {
|
|
370
|
+
throw new PluginError('plugin_event_unroutable', 'Babysitter capability request does not match verified delivery input.');
|
|
371
|
+
}
|
|
372
|
+
return snapshot;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function babysitterReceipt(value: unknown): unknown {
|
|
376
|
+
const snapshot = boundedJsonSnapshot(value, 'Babysitter capability receipt');
|
|
377
|
+
if (typeof snapshot !== 'object' || snapshot === null || ARRAY_IS_ARRAY(snapshot)) {
|
|
378
|
+
throw new PluginError('plugin_unsupported', 'Babysitter capability returned an invalid receipt.');
|
|
379
|
+
}
|
|
380
|
+
const receipt = snapshot as Record<string, unknown>;
|
|
381
|
+
if (!hasExactKeys(receipt, ['receiptId', 'status'])
|
|
382
|
+
|| typeof receipt.receiptId !== 'string' || !matches(RECEIPT_ID, receipt.receiptId)
|
|
383
|
+
|| (receipt.status !== 'queued' && receipt.status !== 'duplicate')) {
|
|
384
|
+
throw new PluginError('plugin_unsupported', 'Babysitter capability returned an invalid receipt.');
|
|
385
|
+
}
|
|
386
|
+
return OBJECT_FREEZE({ receiptId: receipt.receiptId, status: receipt.status });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function matches(pattern: RegExp, value: string): boolean {
|
|
390
|
+
return REGEXP_TEST(pattern, value);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function ownOption<T>(options: object, name: string): T | undefined {
|
|
394
|
+
const descriptor = OBJECT_GET_OWN_PROPERTY_DESCRIPTOR(options, name);
|
|
395
|
+
return descriptor !== undefined && 'value' in descriptor ? descriptor.value as T : undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function record(value: unknown, what: string): Record<string, unknown> {
|
|
399
|
+
if (typeof value !== 'object' || value === null || ARRAY_IS_ARRAY(value)) {
|
|
400
|
+
throw new PluginError('plugin_event_unroutable', `${what} must be an object.`);
|
|
401
|
+
}
|
|
402
|
+
return value as Record<string, unknown>;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function exactRecord(value: unknown, what: string, keys: readonly string[]): Record<string, unknown> {
|
|
406
|
+
const object = record(value, what);
|
|
407
|
+
if (!hasExactKeys(object, keys)) {
|
|
408
|
+
throw new PluginError('plugin_event_unroutable', `${what} has an invalid field set.`);
|
|
409
|
+
}
|
|
410
|
+
return object;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function optionalRecord(
|
|
414
|
+
value: unknown,
|
|
415
|
+
what: string,
|
|
416
|
+
allowed: readonly string[],
|
|
417
|
+
required: readonly string[],
|
|
418
|
+
): Record<string, unknown> {
|
|
419
|
+
const object = record(value, what);
|
|
420
|
+
const actual = OBJECT_KEYS(object);
|
|
421
|
+
for (let index = 0; index < actual.length; index += 1) {
|
|
422
|
+
if (!contains(allowed, actual[index]!)) {
|
|
423
|
+
throw new PluginError('plugin_event_unroutable', `${what} has an invalid field set.`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (let index = 0; index < required.length; index += 1) {
|
|
427
|
+
if (!OBJECT_HAS_OWN(object, required[index]!)) {
|
|
428
|
+
throw new PluginError('plugin_event_unroutable', `${what} has an invalid field set.`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return object;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
435
|
+
if (OBJECT_KEYS(value).length !== keys.length) return false;
|
|
436
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
437
|
+
if (!OBJECT_HAS_OWN(value, keys[index]!)) return false;
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function contains(values: readonly string[], expected: string): boolean {
|
|
443
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
444
|
+
if (values[index] === expected) return true;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function manifestNames(
|
|
450
|
+
matches: readonly { readonly manifest: FlowExtensionManifest }[],
|
|
451
|
+
): string {
|
|
452
|
+
let value = '';
|
|
453
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
454
|
+
value += `${index === 0 ? '' : ', '}${matches[index]!.manifest.name}`;
|
|
455
|
+
}
|
|
456
|
+
return value;
|
|
457
|
+
}
|