runline 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/engine.d.ts +27 -1
- package/dist/core/engine.js +50 -25
- package/dist/index.d.ts +1 -1
- package/dist/plugins/shiftLabs/src/index.js +4 -4
- package/dist/plugins/shiftLabs/src/objects.js +249 -0
- package/dist/plugins/shiftLabs/src/transcription.js +2 -11
- package/dist/plugins/shiftOcr/src/index.js +153 -0
- package/dist/sdk.d.ts +11 -1
- package/dist/sdk.js +6 -2
- package/package.json +4 -5
- package/vendor/README.md +0 -19
- package/vendor/minisearch.umd.js +0 -2013
package/dist/core/engine.d.ts
CHANGED
|
@@ -9,6 +9,30 @@ export interface EngineOptions {
|
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
memoryLimitBytes?: number;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* One resolved action call, observed at the engine's dispatch point.
|
|
14
|
+
*
|
|
15
|
+
* Fired only around `action.execute()` — an unknown path or a failed
|
|
16
|
+
* input validation throws before plugin code runs and is not an
|
|
17
|
+
* invocation. `error` is the thrown value, unmodified, when execute
|
|
18
|
+
* rejected; the hook sees it in the host process even though the
|
|
19
|
+
* worker only ever receives the message string.
|
|
20
|
+
*/
|
|
21
|
+
export interface ActionInvocation {
|
|
22
|
+
plugin: string;
|
|
23
|
+
action: string;
|
|
24
|
+
/** The full callable path, "plugin.action". */
|
|
25
|
+
path: string;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
error?: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface EngineHooks {
|
|
30
|
+
/**
|
|
31
|
+
* Observer for every action invocation. Must not throw — but if it
|
|
32
|
+
* does, the engine swallows it: observability never breaks a run.
|
|
33
|
+
*/
|
|
34
|
+
onAction?: (info: ActionInvocation) => void;
|
|
35
|
+
}
|
|
12
36
|
/**
|
|
13
37
|
* Whether to arm the host-side RSS watchdog for a worker run.
|
|
14
38
|
*
|
|
@@ -48,9 +72,10 @@ export declare function shouldArmRssWatchdog(versions?: Partial<Record<string, s
|
|
|
48
72
|
export declare class ExecutionEngine {
|
|
49
73
|
private registry;
|
|
50
74
|
private config;
|
|
75
|
+
private hooks;
|
|
51
76
|
private pooled;
|
|
52
77
|
private readonly maxRunsPerWorker;
|
|
53
|
-
constructor(registry: PluginRegistry, config: RunlineConfig);
|
|
78
|
+
constructor(registry: PluginRegistry, config: RunlineConfig, hooks?: EngineHooks);
|
|
54
79
|
/** Retire the pooled worker, if any. Safe to call repeatedly. */
|
|
55
80
|
dispose(): void;
|
|
56
81
|
/**
|
|
@@ -71,6 +96,7 @@ export declare class ExecutionEngine {
|
|
|
71
96
|
private release;
|
|
72
97
|
execute(code: string, options?: EngineOptions): Promise<ExecuteResult>;
|
|
73
98
|
private invokeAction;
|
|
99
|
+
private fireOnAction;
|
|
74
100
|
/**
|
|
75
101
|
* A miss on "salesforce.status" is often a plugin that registered
|
|
76
102
|
* its action name with the plugin prefix ("salesforce.status" inside
|
package/dist/core/engine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { applyEnvOverrides, updateConnectionConfig } from "../config/loader.js";
|
|
5
5
|
import { formatValidationError, helpInputs, isTypedInputSchema, validateTypedInput, } from "../plugin/schema.js";
|
|
@@ -81,11 +81,13 @@ const RSS_WATCHDOG_SLACK_BYTES = 128 * 1024 * 1024;
|
|
|
81
81
|
export class ExecutionEngine {
|
|
82
82
|
registry;
|
|
83
83
|
config;
|
|
84
|
+
hooks;
|
|
84
85
|
pooled = null;
|
|
85
86
|
maxRunsPerWorker;
|
|
86
|
-
constructor(registry, config) {
|
|
87
|
+
constructor(registry, config, hooks = {}) {
|
|
87
88
|
this.registry = registry;
|
|
88
89
|
this.config = config;
|
|
90
|
+
this.hooks = hooks;
|
|
89
91
|
this.maxRunsPerWorker =
|
|
90
92
|
config.maxRunsPerWorker ?? DEFAULT_MAX_RUNS_PER_WORKER;
|
|
91
93
|
}
|
|
@@ -320,7 +322,35 @@ export class ExecutionEngine {
|
|
|
320
322
|
throw new Error(formatValidationError(path, validation));
|
|
321
323
|
}
|
|
322
324
|
}
|
|
323
|
-
|
|
325
|
+
const started = performance.now();
|
|
326
|
+
try {
|
|
327
|
+
const value = await action.execute(args, ctx);
|
|
328
|
+
this.fireOnAction(plugin.name, action.name, path, started, false);
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
// `failed` is a flag, not `error !== undefined` — a plugin that
|
|
333
|
+
// does `throw undefined` still failed.
|
|
334
|
+
this.fireOnAction(plugin.name, action.name, path, started, true, err);
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
fireOnAction(pluginName, actionName, path, started, failed, error) {
|
|
339
|
+
const onAction = this.hooks.onAction;
|
|
340
|
+
if (!onAction)
|
|
341
|
+
return;
|
|
342
|
+
try {
|
|
343
|
+
onAction({
|
|
344
|
+
plugin: pluginName,
|
|
345
|
+
action: actionName,
|
|
346
|
+
path,
|
|
347
|
+
durationMs: performance.now() - started,
|
|
348
|
+
...(failed ? { error: error ?? new Error("Unknown error") } : {}),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Observability never breaks a run.
|
|
353
|
+
}
|
|
324
354
|
}
|
|
325
355
|
/**
|
|
326
356
|
* A miss on "salesforce.status" is often a plugin that registered
|
|
@@ -385,14 +415,12 @@ function formatError(cause) {
|
|
|
385
415
|
}
|
|
386
416
|
return String(cause);
|
|
387
417
|
}
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
// See vendor/README.md for the upgrade procedure.
|
|
395
|
-
const minisearchSource = readFileSync(new URL("../../vendor/minisearch.umd.js", import.meta.url), "utf8");
|
|
418
|
+
// ferrosearch is a Rust native addon, so unlike the old minisearch UMD it
|
|
419
|
+
// cannot be inlined into the worker source as text. Resolve its entry point
|
|
420
|
+
// host-side and require it by absolute path inside the worker: eval'd worker
|
|
421
|
+
// code gets a cwd-relative `require`, so a bare specifier would resolve
|
|
422
|
+
// against the caller's cwd instead of runline's own node_modules.
|
|
423
|
+
const ferrosearchPath = createRequire(import.meta.url).resolve("@shift-labs/ferrosearch");
|
|
396
424
|
function buildHelpData(plugins) {
|
|
397
425
|
const data = {};
|
|
398
426
|
for (const p of plugins) {
|
|
@@ -432,7 +460,7 @@ function buildWorkerSource(pluginNames = [], helpData = {}) {
|
|
|
432
460
|
"console",
|
|
433
461
|
"fetch",
|
|
434
462
|
"require",
|
|
435
|
-
"
|
|
463
|
+
"FerroSearch",
|
|
436
464
|
];
|
|
437
465
|
const wrapped = `"use strict";
|
|
438
466
|
const { parentPort: __port } = require("node:worker_threads");
|
|
@@ -522,14 +550,11 @@ const __fmtErr = (e) => {
|
|
|
522
550
|
// structured-clone-safe and preserves JSON value semantics.
|
|
523
551
|
const __toJson = (v) => v === undefined ? undefined : JSON.parse(JSON.stringify(v));
|
|
524
552
|
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
${minisearchSource}
|
|
531
|
-
return module.exports;
|
|
532
|
-
})();
|
|
553
|
+
// ferrosearch native addon, resolved to an absolute path by the host. Its
|
|
554
|
+
// index memory lives outside the V8 heap, which is the point — minisearch's
|
|
555
|
+
// JS index counted against maxOldGenerationSizeMb and grew worker RSS — but
|
|
556
|
+
// also means an index built by agent code is invisible to that limit.
|
|
557
|
+
const { FerroSearch } = require(${JSON.stringify(ferrosearchPath)});
|
|
533
558
|
|
|
534
559
|
const __help = ${JSON.stringify(helpData)};
|
|
535
560
|
|
|
@@ -563,7 +588,7 @@ const __formatSignature = (plugin, entry) => {
|
|
|
563
588
|
return plugin + '.' + entry.action + (fields ? '({ ' + fields + ' })' : '()');
|
|
564
589
|
};
|
|
565
590
|
|
|
566
|
-
// Build a
|
|
591
|
+
// Build a FerroSearch index over every action path. Indexed at worker
|
|
567
592
|
// startup, queried by actions.find().
|
|
568
593
|
const __search = (() => {
|
|
569
594
|
const docs = [];
|
|
@@ -577,7 +602,7 @@ const __search = (() => {
|
|
|
577
602
|
description: entry.description || '',
|
|
578
603
|
});
|
|
579
604
|
}
|
|
580
|
-
const
|
|
605
|
+
const index = new FerroSearch({
|
|
581
606
|
fields: ['path', 'plugin', 'action', 'description'],
|
|
582
607
|
storeFields: ['path', 'description'],
|
|
583
608
|
searchOptions: {
|
|
@@ -586,8 +611,8 @@ const __search = (() => {
|
|
|
586
611
|
boost: { path: 3, action: 2, plugin: 2 },
|
|
587
612
|
},
|
|
588
613
|
});
|
|
589
|
-
|
|
590
|
-
return
|
|
614
|
+
index.addAll(docs);
|
|
615
|
+
return index;
|
|
591
616
|
})();
|
|
592
617
|
|
|
593
618
|
const __actionsApi = {
|
|
@@ -813,7 +838,7 @@ const __injectValues = __injectNames.map((n) => {
|
|
|
813
838
|
case "console": return console;
|
|
814
839
|
case "fetch": return fetch;
|
|
815
840
|
case "require": return __bodyRequire;
|
|
816
|
-
case "
|
|
841
|
+
case "FerroSearch": return FerroSearch;
|
|
817
842
|
default: return __makeProxy([n]);
|
|
818
843
|
}
|
|
819
844
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { addConnection, findConfigDir, getConnection, loadConfig, removeConnection, saveConfig, updateConnectionConfig, } from "./config/loader.js";
|
|
2
2
|
export type { RunlineConfig } from "./config/types.js";
|
|
3
3
|
export { DEFAULT_CONFIG } from "./config/types.js";
|
|
4
|
-
export type { EngineOptions, ExecuteResult } from "./core/engine.js";
|
|
4
|
+
export type { ActionInvocation, EngineHooks, EngineOptions, ExecuteResult, } from "./core/engine.js";
|
|
5
5
|
export { ExecutionEngine } from "./core/engine.js";
|
|
6
6
|
export type { BuildAuthUrlOptions, ExchangeCodeOptions, OAuthTokens, PKCEPair, RunOAuthOptions, } from "./core/oauth.js";
|
|
7
7
|
export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_URI, runOAuth, } from "./core/oauth.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
2
|
import { registerIssueViewActions } from "./issue-views.js";
|
|
3
3
|
import { registerIssueActions } from "./issues.js";
|
|
4
|
+
import { registerObjectActions } from "./objects.js";
|
|
4
5
|
import { registerPageActions } from "./pages.js";
|
|
5
6
|
import { registerProjectActions } from "./projects.js";
|
|
6
7
|
import { registerTranscriptionActions } from "./transcription.js";
|
|
@@ -8,18 +9,17 @@ export default function shiftLabs(rl) {
|
|
|
8
9
|
rl.setName("shiftLabs");
|
|
9
10
|
rl.setVersion("0.1.0");
|
|
10
11
|
rl.setConnectionSchema(t.Object({
|
|
12
|
+
// The API key is the tenant authority for every action; the cloud
|
|
13
|
+
// derives the organization from it (SHFT-852), so no org ID is needed.
|
|
11
14
|
apiKey: t.String({
|
|
12
15
|
description: "Shift Labs API key",
|
|
13
16
|
env: "SHIFT_LABS_API_KEY",
|
|
14
17
|
}),
|
|
15
|
-
organizationId: t.Optional(t.String({
|
|
16
|
-
description: "Shift Labs organization ID (required for transcription actions)",
|
|
17
|
-
env: "SHIFT_LABS_ORG_ID",
|
|
18
|
-
})),
|
|
19
18
|
}));
|
|
20
19
|
registerProjectActions(rl);
|
|
21
20
|
registerIssueActions(rl);
|
|
22
21
|
registerIssueViewActions(rl);
|
|
23
22
|
registerPageActions(rl);
|
|
24
23
|
registerTranscriptionActions(rl);
|
|
24
|
+
registerObjectActions(rl);
|
|
25
25
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname } from "node:path";
|
|
4
|
+
import * as t from "typebox";
|
|
5
|
+
import { enumSchema, pathSegment, request } from "./shared.js";
|
|
6
|
+
const OBJECTS_BASE = "/v1/services/objects";
|
|
7
|
+
export const OBJECT_STATUS = ["pending_upload", "ready", "archived"];
|
|
8
|
+
export const OBJECT_LINK_TARGET = [
|
|
9
|
+
"session",
|
|
10
|
+
"artifact",
|
|
11
|
+
"eval_case",
|
|
12
|
+
"db_record",
|
|
13
|
+
];
|
|
14
|
+
/** Mirrors the public service contract's 5 GiB object ceiling. */
|
|
15
|
+
const MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024;
|
|
16
|
+
const STRICT_OBJECT = { additionalProperties: false };
|
|
17
|
+
const Id = t.String({ minLength: 1, pattern: "\\S" });
|
|
18
|
+
const MEDIA_TYPES = {
|
|
19
|
+
csv: "text/csv",
|
|
20
|
+
doc: "application/msword",
|
|
21
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
22
|
+
gif: "image/gif",
|
|
23
|
+
html: "text/html",
|
|
24
|
+
jpeg: "image/jpeg",
|
|
25
|
+
jpg: "image/jpeg",
|
|
26
|
+
json: "application/json",
|
|
27
|
+
md: "text/markdown",
|
|
28
|
+
mp3: "audio/mpeg",
|
|
29
|
+
mp4: "video/mp4",
|
|
30
|
+
pdf: "application/pdf",
|
|
31
|
+
png: "image/png",
|
|
32
|
+
txt: "text/plain",
|
|
33
|
+
wav: "audio/wav",
|
|
34
|
+
webp: "image/webp",
|
|
35
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
36
|
+
zip: "application/zip",
|
|
37
|
+
};
|
|
38
|
+
function mediaTypeFor(path, override) {
|
|
39
|
+
if (override)
|
|
40
|
+
return override;
|
|
41
|
+
return (MEDIA_TYPES[extname(path).slice(1).toLowerCase()] ??
|
|
42
|
+
"application/octet-stream");
|
|
43
|
+
}
|
|
44
|
+
async function fileBody(path, sizeBytes) {
|
|
45
|
+
try {
|
|
46
|
+
return await openAsBlob(path);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Runtimes without fs.openAsBlob fall back to buffering; refuse to
|
|
50
|
+
// buffer files that would strain the host process.
|
|
51
|
+
if (sizeBytes > 512 * 1024 * 1024) {
|
|
52
|
+
throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
|
|
53
|
+
}
|
|
54
|
+
return await readFile(path);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function linkSchema() {
|
|
58
|
+
return t.Object({
|
|
59
|
+
targetType: enumSchema("Link target type", OBJECT_LINK_TARGET),
|
|
60
|
+
targetId: t.String({
|
|
61
|
+
minLength: 1,
|
|
62
|
+
maxLength: 500,
|
|
63
|
+
pattern: "\\S",
|
|
64
|
+
description: "Target ID; db_record targets use source_system/table/row style ids, e.g. R2M_Sys/GoodsReceiptLog/42",
|
|
65
|
+
}),
|
|
66
|
+
role: t.Optional(t.String({
|
|
67
|
+
minLength: 1,
|
|
68
|
+
maxLength: 200,
|
|
69
|
+
pattern: "\\S",
|
|
70
|
+
description: "Link role, e.g. source",
|
|
71
|
+
})),
|
|
72
|
+
}, STRICT_OBJECT);
|
|
73
|
+
}
|
|
74
|
+
const provenanceFields = {
|
|
75
|
+
workspaceId: t.Optional(Id),
|
|
76
|
+
sessionId: t.Optional(Id),
|
|
77
|
+
artifactId: t.Optional(Id),
|
|
78
|
+
};
|
|
79
|
+
export function registerObjectActions(rl) {
|
|
80
|
+
rl.registerAction("object.upload", {
|
|
81
|
+
access: "write",
|
|
82
|
+
description: "Persist a local binary file (image, PDF, document, export) in the " +
|
|
83
|
+
"organization's durable object bucket. Uploads through a short-lived " +
|
|
84
|
+
"signed grant and returns the ready object with its stable ID — use " +
|
|
85
|
+
"that ID as a durable sourceObjectId in DB rows and artifact links.",
|
|
86
|
+
inputSchema: t.Object({
|
|
87
|
+
path: t.String({
|
|
88
|
+
minLength: 1,
|
|
89
|
+
pattern: "\\S",
|
|
90
|
+
description: "Host path of the file to store",
|
|
91
|
+
}),
|
|
92
|
+
contentType: t.Optional(t.String({
|
|
93
|
+
minLength: 1,
|
|
94
|
+
maxLength: 200,
|
|
95
|
+
pattern: "\\S",
|
|
96
|
+
description: "Explicit MIME type; inferred from the extension when omitted",
|
|
97
|
+
})),
|
|
98
|
+
filename: t.Optional(t.String({
|
|
99
|
+
minLength: 1,
|
|
100
|
+
maxLength: 500,
|
|
101
|
+
pattern: "\\S",
|
|
102
|
+
description: "Original filename; defaults to the path's basename",
|
|
103
|
+
})),
|
|
104
|
+
metadata: t.Optional(t.Record(t.String(), t.Unknown(), {
|
|
105
|
+
description: "Small caller-defined metadata object",
|
|
106
|
+
})),
|
|
107
|
+
...provenanceFields,
|
|
108
|
+
}, STRICT_OBJECT),
|
|
109
|
+
async execute(input, ctx) {
|
|
110
|
+
const fields = input;
|
|
111
|
+
const contentType = mediaTypeFor(fields.path, fields.contentType);
|
|
112
|
+
const file = await stat(fields.path);
|
|
113
|
+
if (!file.isFile())
|
|
114
|
+
throw new Error(`${fields.path} is not a file`);
|
|
115
|
+
if (file.size === 0)
|
|
116
|
+
throw new Error("File is empty");
|
|
117
|
+
if (file.size > MAX_OBJECT_BYTES) {
|
|
118
|
+
throw new Error(`File is larger than the ${MAX_OBJECT_BYTES}-byte service limit`);
|
|
119
|
+
}
|
|
120
|
+
const created = await request(ctx, `${OBJECTS_BASE}/objects`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
// The API key is the tenant authority; no organizationId is sent.
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
contentType,
|
|
125
|
+
sizeBytes: file.size,
|
|
126
|
+
filename: fields.filename ?? basename(fields.path),
|
|
127
|
+
workspaceId: fields.workspaceId,
|
|
128
|
+
sessionId: fields.sessionId,
|
|
129
|
+
artifactId: fields.artifactId,
|
|
130
|
+
metadata: fields.metadata,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
const uploadHeaders = new Headers(created.upload.headers);
|
|
134
|
+
if (!uploadHeaders.has("content-type")) {
|
|
135
|
+
uploadHeaders.set("content-type", contentType);
|
|
136
|
+
}
|
|
137
|
+
const upload = await fetch(created.upload.url, {
|
|
138
|
+
method: created.upload.method,
|
|
139
|
+
headers: uploadHeaders,
|
|
140
|
+
body: await fileBody(fields.path, file.size),
|
|
141
|
+
});
|
|
142
|
+
if (!upload.ok) {
|
|
143
|
+
throw new Error(`Object upload failed: HTTP ${upload.status}`);
|
|
144
|
+
}
|
|
145
|
+
return request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(created.object.id)}/complete`, { method: "POST" });
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
rl.registerAction("object.list", {
|
|
149
|
+
access: "read",
|
|
150
|
+
description: "List stored objects, newest first. Archived objects appear only " +
|
|
151
|
+
"when filtered for by status.",
|
|
152
|
+
inputSchema: t.Object({
|
|
153
|
+
status: t.Optional(enumSchema("Object status", OBJECT_STATUS)),
|
|
154
|
+
contentType: t.Optional(t.String({ minLength: 1, maxLength: 200, pattern: "\\S" })),
|
|
155
|
+
...provenanceFields,
|
|
156
|
+
}, STRICT_OBJECT),
|
|
157
|
+
async execute(input, ctx) {
|
|
158
|
+
const params = new URLSearchParams();
|
|
159
|
+
for (const [key, value] of Object.entries(input)) {
|
|
160
|
+
if (value !== undefined)
|
|
161
|
+
params.set(key, String(value));
|
|
162
|
+
}
|
|
163
|
+
const query = params.toString();
|
|
164
|
+
const body = await request(ctx, `${OBJECTS_BASE}/objects${query ? `?${query}` : ""}`);
|
|
165
|
+
return body.objects;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
rl.registerAction("object.get", {
|
|
169
|
+
access: "read",
|
|
170
|
+
description: "Get a stored object's metadata by ID.",
|
|
171
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
172
|
+
async execute(input, ctx) {
|
|
173
|
+
const { id } = input;
|
|
174
|
+
return request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(id)}`);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
rl.registerAction("object.download", {
|
|
178
|
+
access: "read",
|
|
179
|
+
description: "Mint a short-lived signed download URL for a ready object. Set " +
|
|
180
|
+
"savePath to also download the bytes to a host file.",
|
|
181
|
+
inputSchema: t.Object({
|
|
182
|
+
id: Id,
|
|
183
|
+
savePath: t.Optional(t.String({
|
|
184
|
+
minLength: 1,
|
|
185
|
+
pattern: "\\S",
|
|
186
|
+
description: "Host path to write the downloaded bytes to",
|
|
187
|
+
})),
|
|
188
|
+
}, STRICT_OBJECT),
|
|
189
|
+
async execute(input, ctx) {
|
|
190
|
+
const fields = input;
|
|
191
|
+
const grant = await request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(fields.id)}/download`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
});
|
|
194
|
+
if (!fields.savePath)
|
|
195
|
+
return grant;
|
|
196
|
+
const response = await fetch(grant.url, { method: grant.method });
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
throw new Error(`Object download failed: HTTP ${response.status}`);
|
|
199
|
+
}
|
|
200
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
201
|
+
await writeFile(fields.savePath, bytes);
|
|
202
|
+
return { savedTo: fields.savePath, sizeBytes: bytes.byteLength };
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
rl.registerAction("object.attach", {
|
|
206
|
+
access: "write",
|
|
207
|
+
description: "Link an object to what produced or consumed it: a session, " +
|
|
208
|
+
"artifact, eval case, or customer DB record. Links are append-only " +
|
|
209
|
+
"provenance; duplicates are rejected.",
|
|
210
|
+
inputSchema: t.Object({
|
|
211
|
+
id: Id,
|
|
212
|
+
links: t.Array(linkSchema(), { minItems: 1, maxItems: 20 }),
|
|
213
|
+
}, STRICT_OBJECT),
|
|
214
|
+
async execute(input, ctx) {
|
|
215
|
+
const fields = input;
|
|
216
|
+
const body = await request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(fields.id)}/links`, { method: "POST", body: JSON.stringify({ links: fields.links }) });
|
|
217
|
+
return body.links;
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
rl.registerAction("object.links", {
|
|
221
|
+
access: "read",
|
|
222
|
+
description: "List an object's provenance links.",
|
|
223
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
224
|
+
async execute(input, ctx) {
|
|
225
|
+
const { id } = input;
|
|
226
|
+
const body = await request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(id)}/links`);
|
|
227
|
+
return body.links;
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
rl.registerAction("object.archive", {
|
|
231
|
+
access: "write",
|
|
232
|
+
description: "Archive a stored object (idempotent). Archived objects drop out of " +
|
|
233
|
+
"default listings and refuse download grants; nothing is deleted.",
|
|
234
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
235
|
+
async execute(input, ctx) {
|
|
236
|
+
const { id } = input;
|
|
237
|
+
return request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(id)}/archive`, { method: "POST" });
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
rl.registerAction("object.bucket", {
|
|
241
|
+
access: "read",
|
|
242
|
+
description: "Get the organization's durable object bucket, provisioning it on " +
|
|
243
|
+
"first use.",
|
|
244
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
245
|
+
async execute(_input, ctx) {
|
|
246
|
+
return request(ctx, `${OBJECTS_BASE}/bucket`);
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
@@ -25,13 +25,6 @@ const MAX_MEDIA_BYTES = 5 * 1024 * 1024 * 1024;
|
|
|
25
25
|
const MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
26
26
|
const STRICT_OBJECT = { additionalProperties: false };
|
|
27
27
|
const Id = t.String({ minLength: 1, pattern: "\\S" });
|
|
28
|
-
function organizationId(ctx) {
|
|
29
|
-
const value = ctx.connection.config.organizationId;
|
|
30
|
-
if (typeof value !== "string" || !value) {
|
|
31
|
-
throw new Error("SHIFT_LABS_ORG_ID is not set. Transcription actions need the organization ID that owns the API key.");
|
|
32
|
-
}
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
28
|
function mediaTypeFor(path) {
|
|
36
29
|
const mediaType = MEDIA_TYPES[extname(path).slice(1).toLowerCase()];
|
|
37
30
|
if (!mediaType) {
|
|
@@ -83,7 +76,6 @@ export function registerTranscriptionActions(rl) {
|
|
|
83
76
|
}, STRICT_OBJECT),
|
|
84
77
|
async execute(input, ctx) {
|
|
85
78
|
const fields = input;
|
|
86
|
-
const org = organizationId(ctx);
|
|
87
79
|
const contentType = mediaTypeFor(fields.path);
|
|
88
80
|
const file = await stat(fields.path);
|
|
89
81
|
if (!file.isFile())
|
|
@@ -95,8 +87,8 @@ export function registerTranscriptionActions(rl) {
|
|
|
95
87
|
}
|
|
96
88
|
const created = await request(ctx, "/v1/services/transcription/assets", {
|
|
97
89
|
method: "POST",
|
|
90
|
+
// The API key is the tenant authority; no organizationId is sent.
|
|
98
91
|
body: JSON.stringify({
|
|
99
|
-
organizationId: org,
|
|
100
92
|
contentType,
|
|
101
93
|
sizeBytes: file.size,
|
|
102
94
|
}),
|
|
@@ -117,7 +109,6 @@ export function registerTranscriptionActions(rl) {
|
|
|
117
109
|
const job = await request(ctx, "/v1/services/transcription/jobs", {
|
|
118
110
|
method: "POST",
|
|
119
111
|
body: JSON.stringify({
|
|
120
|
-
organizationId: org,
|
|
121
112
|
assetId: created.asset.id,
|
|
122
113
|
language: fields.language,
|
|
123
114
|
diarize: fields.diarize,
|
|
@@ -135,7 +126,7 @@ export function registerTranscriptionActions(rl) {
|
|
|
135
126
|
description: "List transcription jobs for the API key's organization, newest first.",
|
|
136
127
|
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
137
128
|
async execute(_input, ctx) {
|
|
138
|
-
const body = await request(ctx,
|
|
129
|
+
const body = await request(ctx, "/v1/services/transcription/jobs");
|
|
139
130
|
return body.jobs;
|
|
140
131
|
},
|
|
141
132
|
});
|