@isomorph.ai/app-sdk 2.1.0 → 2.3.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/computer-use-handoff.d.ts +5 -2
- package/dist/computer-use-handoff.js +5 -5
- package/dist/index.d.ts +20 -1
- package/dist/index.js +5 -2
- package/dist/jobs.d.ts +3 -0
- package/dist/jobs.js +36 -6
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +6 -4
- package/package.json +1 -1
|
@@ -91,14 +91,17 @@ export interface ComputerUseLiveConnection {
|
|
|
91
91
|
};
|
|
92
92
|
viewerPath: string;
|
|
93
93
|
}
|
|
94
|
+
export type ComputerUseTakeover = ComputerUsePause & {
|
|
95
|
+
connection?: ComputerUseLiveConnection;
|
|
96
|
+
};
|
|
94
97
|
export interface ComputerUseHandoffOptions extends ComputerUseTransport {
|
|
95
98
|
runKey: string;
|
|
96
99
|
authorize(): Promise<boolean> | boolean;
|
|
97
100
|
}
|
|
98
101
|
export declare function createComputerUseHandoff(options: ComputerUseHandoffOptions): {
|
|
99
|
-
requestTakeover(reason: string, requiredInput?: "login" | "human"): Promise<
|
|
102
|
+
requestTakeover(reason: string, requiredInput?: "login" | "human", pauseId?: string): Promise<ComputerUseTakeover>;
|
|
100
103
|
view(pauseId: string): Promise<ComputerUsePage>;
|
|
101
|
-
live(
|
|
104
|
+
live(_pauseId?: string): Promise<ComputerUseLiveConnection>;
|
|
102
105
|
input(pauseId: string, action: ComputerUseInputAction): Promise<ComputerUseInputResult>;
|
|
103
106
|
completeLogin(pauseId: string, input: ComputerUseLoginInput): Promise<{
|
|
104
107
|
status: "login_queued";
|
|
@@ -32,7 +32,7 @@ export function waitingInput(answer) {
|
|
|
32
32
|
}
|
|
33
33
|
export function createComputerUseHandoff(options) {
|
|
34
34
|
if (typeof options.runKey !== 'string' || !options.runKey || typeof options.authorize !== 'function')
|
|
35
|
-
throw new Error('Computer use handoff needs runKey and an authorize check for the current
|
|
35
|
+
throw new Error('Computer use handoff needs runKey and an authorize check for the current app task.');
|
|
36
36
|
const api = computerUseApi(options);
|
|
37
37
|
const post = async (path, body = {}, privateInput = false) => {
|
|
38
38
|
if (await options.authorize() !== true)
|
|
@@ -61,10 +61,10 @@ export function createComputerUseHandoff(options) {
|
|
|
61
61
|
return { pauseId };
|
|
62
62
|
};
|
|
63
63
|
return {
|
|
64
|
-
requestTakeover(reason, requiredInput = 'human') {
|
|
64
|
+
requestTakeover(reason, requiredInput = 'human', pauseId) {
|
|
65
65
|
if (!reason.trim() || !['login', 'human'].includes(requiredInput))
|
|
66
66
|
throw new Error('Takeover needs a reason and login or human input.');
|
|
67
|
-
return post('/tasks/takeover', { reason, requiredInput }).then(pauseOf);
|
|
67
|
+
return post('/tasks/takeover', { reason, requiredInput, ...(pauseId ? paused(pauseId) : {}) }).then(answer => ({ ...pauseOf(answer), ...(answer.connection ? { connection: answer.connection } : {}) }));
|
|
68
68
|
},
|
|
69
69
|
// The API answers 409 while the task is still pausing or after it resumed; `ready` is the only 200.
|
|
70
70
|
async view(pauseId) {
|
|
@@ -73,8 +73,8 @@ export function createComputerUseHandoff(options) {
|
|
|
73
73
|
throw new Error('The computer use API returned an incomplete screen.');
|
|
74
74
|
return { status: 'ready', screenshot: page.screenshot, url: page.url };
|
|
75
75
|
},
|
|
76
|
-
live(
|
|
77
|
-
return post('/tasks/live'
|
|
76
|
+
live(_pauseId) {
|
|
77
|
+
return post('/tasks/live');
|
|
78
78
|
},
|
|
79
79
|
input(pauseId, action) {
|
|
80
80
|
return post('/tasks/input', { ...paused(pauseId), action });
|
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,23 @@ export interface IsomorphUser {
|
|
|
36
36
|
*/
|
|
37
37
|
role: IsomorphUserRole;
|
|
38
38
|
appUser?: unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Who is calling: a `'person'` signed in through the browser or an MCP
|
|
41
|
+
* client, or a `'service'` holding a credential the company's IT issued,
|
|
42
|
+
* whose `id` and `name` are the credential's. Absent means a person.
|
|
43
|
+
*/
|
|
44
|
+
kind?: 'person' | 'service';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `export const expose = { description, input }` on an action, beside
|
|
48
|
+
* `export const trusted = true`: the action may be called from outside the
|
|
49
|
+
* app by a service credential or an MCP client. `description` is read by
|
|
50
|
+
* people and agents; `input` is a JSON Schema object literal the platform
|
|
51
|
+
* validates the request body against. Literal only.
|
|
52
|
+
*/
|
|
53
|
+
export interface ActionExposure {
|
|
54
|
+
description: string;
|
|
55
|
+
input: Record<string, unknown>;
|
|
39
56
|
}
|
|
40
57
|
export interface IsomorphResult<T> {
|
|
41
58
|
data: T;
|
|
@@ -2018,7 +2035,9 @@ export declare function createClient(options?: ClientOptions): {
|
|
|
2018
2035
|
watch: <T = Record<string, unknown>>(build: () => QueryBuilder<T, any>, onResult: (result: WatchResult) => void, options?: WatchOptions) => (() => void);
|
|
2019
2036
|
};
|
|
2020
2037
|
computerView: {
|
|
2021
|
-
url(action: string, taskId: string
|
|
2038
|
+
url(action: string, taskId: string, options?: {
|
|
2039
|
+
mode?: "watch" | "control";
|
|
2040
|
+
}): string;
|
|
2022
2041
|
};
|
|
2023
2042
|
files: {
|
|
2024
2043
|
createUpload: (input: {
|
package/dist/index.js
CHANGED
|
@@ -774,10 +774,13 @@ export function createClient(options = {}) {
|
|
|
774
774
|
},
|
|
775
775
|
},
|
|
776
776
|
computerView: {
|
|
777
|
-
url(action, taskId) {
|
|
777
|
+
url(action, taskId, options = {}) {
|
|
778
778
|
if (!/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(action) || !/^[A-Za-z0-9_-]{1,128}$/.test(taskId))
|
|
779
779
|
throw new IsomorphError('VALIDATION_FAILED', 'Computer view needs an action and task ID.');
|
|
780
|
-
|
|
780
|
+
const mode = options.mode ?? 'watch';
|
|
781
|
+
if (!['watch', 'control'].includes(mode))
|
|
782
|
+
throw new IsomorphError('VALIDATION_FAILED', 'Computer view needs watch or control mode.');
|
|
783
|
+
return `${transport.baseUrl}/_harbour/computer-view/${encodeURIComponent(action)}/${encodeURIComponent(taskId)}/${mode}/`;
|
|
781
784
|
},
|
|
782
785
|
},
|
|
783
786
|
files: {
|
package/dist/jobs.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActionExposure } from './index.js';
|
|
1
2
|
export declare const JOBS_DIR = "jobs";
|
|
2
3
|
export interface EmailArrivalTrigger {
|
|
3
4
|
type: 'email.arrived';
|
|
@@ -25,6 +26,8 @@ export interface JobDefinition {
|
|
|
25
26
|
schedule?: string;
|
|
26
27
|
trigger?: EventTrigger;
|
|
27
28
|
}
|
|
29
|
+
/** The action's `export const expose = { description, input }`, read without importing it: undefined when the module has none, does not parse, or declares anything but a literal object of that shape. */
|
|
30
|
+
export declare function actionExposure(file: string): Promise<ActionExposure | undefined>;
|
|
28
31
|
export declare function eventTrigger(value: unknown): EventTrigger;
|
|
29
32
|
export interface JobContext {
|
|
30
33
|
scheduledAt: string;
|
package/dist/jobs.js
CHANGED
|
@@ -8,25 +8,55 @@ export const JOBS_DIR = 'jobs';
|
|
|
8
8
|
const LOGICAL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9
9
|
export const FILE_MIME_TYPES = ['application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'image/png', 'image/jpeg'];
|
|
10
10
|
export const providerFileId = (value) => typeof value === 'string' && /^[A-Za-z0-9!._,=-]{1,256}$/.test(value);
|
|
11
|
-
function literalObject(node) {
|
|
11
|
+
function literalObject(node, what = 'trigger') {
|
|
12
12
|
const value = node;
|
|
13
13
|
if (value?.type === 'Literal')
|
|
14
14
|
return value.value;
|
|
15
15
|
if (value?.type === 'ArrayExpression')
|
|
16
|
-
return value.elements.map(literalObject);
|
|
16
|
+
return value.elements.map(element => literalObject(element, what));
|
|
17
17
|
if (value?.type !== 'ObjectExpression')
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error(`${what} must be a literal object`);
|
|
19
19
|
const result = {};
|
|
20
20
|
for (const property of value.properties ?? []) {
|
|
21
21
|
if (property.type !== 'Property' || property.computed || property.kind !== 'init' || property.method)
|
|
22
|
-
throw new Error(
|
|
22
|
+
throw new Error(`${what} must be a literal object`);
|
|
23
23
|
const key = property.key.name ?? property.key.value;
|
|
24
24
|
if (typeof key !== 'string' || Object.hasOwn(result, key))
|
|
25
|
-
throw new Error(
|
|
26
|
-
Object.defineProperty(result, key, { value: literalObject(property.value), enumerable: true });
|
|
25
|
+
throw new Error(`${what} contains an invalid or repeated field`);
|
|
26
|
+
Object.defineProperty(result, key, { value: literalObject(property.value, what), enumerable: true });
|
|
27
27
|
}
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
|
+
const plainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
31
|
+
/** The action's `export const expose = { description, input }`, read without importing it: undefined when the module has none, does not parse, or declares anything but a literal object of that shape. */
|
|
32
|
+
export async function actionExposure(file) {
|
|
33
|
+
let statements;
|
|
34
|
+
try {
|
|
35
|
+
statements = parseModule(await readFile(file, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
for (const statement of statements) {
|
|
41
|
+
if (statement.type !== 'ExportNamedDeclaration' || statement.declaration?.type !== 'VariableDeclaration' || statement.declaration.kind !== 'const')
|
|
42
|
+
continue;
|
|
43
|
+
for (const declaration of statement.declaration.declarations) {
|
|
44
|
+
if (declaration.id.type !== 'Identifier' || declaration.id.name !== 'expose')
|
|
45
|
+
continue;
|
|
46
|
+
let value;
|
|
47
|
+
try {
|
|
48
|
+
value = literalObject(declaration.init, 'expose');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!plainObject(value) || Object.keys(value).some(key => key !== 'description' && key !== 'input') || typeof value.description !== 'string' || !plainObject(value.input))
|
|
54
|
+
return undefined;
|
|
55
|
+
return { description: value.description, input: value.input };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
30
60
|
export function eventTrigger(value) {
|
|
31
61
|
if (value?.type === 'file.created') {
|
|
32
62
|
const t = value;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
-
import { type IsomorphClient, type IsomorphUser, type JobRun, type JobRunSummary } from './index.js';
|
|
2
|
+
import { type ActionExposure, type IsomorphClient, type IsomorphUser, type JobRun, type JobRunSummary } from './index.js';
|
|
3
3
|
import { type JobDefinition } from './jobs.js';
|
|
4
4
|
import { type LocalGatewayOptions } from './local-gateway.js';
|
|
5
5
|
export { JOBS_DIR, type JobContext } from './jobs.js';
|
|
@@ -37,6 +37,8 @@ export interface RuntimeInventory {
|
|
|
37
37
|
actions: string[];
|
|
38
38
|
/** The actions whose module says `export const trusted = true`: they act as the app on behalf of the caller. */
|
|
39
39
|
trusted: string[];
|
|
40
|
+
/** The actions whose module says `export const expose = { description, input }`, by name: what the kit gate records as exposed (ADR 0100). */
|
|
41
|
+
exposed: Record<string, ActionExposure>;
|
|
40
42
|
jobs: JobDefinition[];
|
|
41
43
|
}
|
|
42
44
|
/** Every `actions/<name>.ts` and `jobs/<name>.ts` the app carries, by logical name. */
|
package/dist/runtime.js
CHANGED
|
@@ -68,7 +68,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
68
68
|
import { createClient, gatewayRequest, IsomorphError } from './index.js';
|
|
69
69
|
import { userGroupsFromHeader } from './user-groups.js';
|
|
70
70
|
import { userRole } from './user-role.js';
|
|
71
|
-
import { FILE_MIME_TYPES, providerFileId, discoverJobs, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
|
|
71
|
+
import { FILE_MIME_TYPES, providerFileId, actionExposure, discoverJobs, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
|
|
72
72
|
import { LocalGateway } from './local-gateway.js';
|
|
73
73
|
export { JOBS_DIR } from './jobs.js';
|
|
74
74
|
const actionIdentity = new AsyncLocalStorage();
|
|
@@ -118,8 +118,10 @@ export async function discover(root) {
|
|
|
118
118
|
};
|
|
119
119
|
const actions = (await list(ACTIONS_DIR)).filter((name) => !name.startsWith(RESERVED_PREFIX));
|
|
120
120
|
const trusted = await Promise.all(actions.map((name) => exportsTrue(join(root, ACTIONS_DIR, `${name}.ts`), 'trusted')));
|
|
121
|
+
const exposures = await Promise.all(actions.map((name) => actionExposure(join(root, ACTIONS_DIR, `${name}.ts`))));
|
|
122
|
+
const exposed = Object.fromEntries(actions.flatMap((name, index) => exposures[index] ? [[name, exposures[index]]] : []));
|
|
121
123
|
const jobs = await discoverJobs(root);
|
|
122
|
-
return { actions, trusted: actions.filter((_, index) => trusted[index]), jobs };
|
|
124
|
+
return { actions, trusted: actions.filter((_, index) => trusted[index]), exposed, jobs };
|
|
123
125
|
}
|
|
124
126
|
// ---- Cron ------------------------------------------------------------------
|
|
125
127
|
/** One field of a five-field cron expression: numbers, `*`, lists, ranges and steps, no names. */
|
|
@@ -313,7 +315,7 @@ export class Runs {
|
|
|
313
315
|
}
|
|
314
316
|
export class Runtime {
|
|
315
317
|
options;
|
|
316
|
-
inventory = { actions: [], trusted: [], jobs: [] };
|
|
318
|
+
inventory = { actions: [], trusted: [], exposed: {}, jobs: [] };
|
|
317
319
|
localGateway;
|
|
318
320
|
running = new Set();
|
|
319
321
|
timers = [];
|
|
@@ -826,7 +828,7 @@ export class Runtime {
|
|
|
826
828
|
const owner = context.user.id;
|
|
827
829
|
switch (name) {
|
|
828
830
|
case PING_ACTION: {
|
|
829
|
-
const answer = { ok: true, mode: this.options.mode, actions: this.inventory.actions, trusted: this.inventory.trusted, jobs: this.inventory.jobs.map((job) => job.name) };
|
|
831
|
+
const answer = { ok: true, mode: this.options.mode, actions: this.inventory.actions, trusted: this.inventory.trusted, exposed: this.inventory.exposed, jobs: this.inventory.jobs.map((job) => job.name) };
|
|
830
832
|
return this.options.mode === 'gate' ? { ...answer, loadFailures: await this.loadFailures() } : answer;
|
|
831
833
|
}
|
|
832
834
|
case ENQUEUE_ACTION: {
|