lua-cli 3.45.0 → 3.46.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/api-exports.d.ts +505 -468
- package/dist/api-exports.js +891 -236
- package/dist/api-exports.js.map +1 -1
- package/dist/index.js +1989 -1000
- package/dist/index.js.map +1 -1
- package/dist/voice/test/index.d.ts +462 -462
- package/dist/workflow-builder.js +656 -46
- package/dist/workflow-builder.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/api/Agents.md +6 -1
- package/docs/workflows/workspaces-and-long-steps.md +2 -0
- package/package.json +1 -1
- package/template/package.json +1 -1
- package/template-web-app/web/package.json +1 -0
- package/template-web-app/web/src/App.tsx +1 -1
- package/template-web-app/web/src/main.tsx +1 -1
- package/template-web-app/web/src/lua/client.ts +0 -269
package/docs/README.md
CHANGED
package/docs/api/Agents.md
CHANGED
|
@@ -81,6 +81,8 @@ await Agents.workflows('base_agent-tp').deploy('wf_123', '1.4.0', { expectActive
|
|
|
81
81
|
await Agents.versions('base_agent-tp').delete(5);
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
> **Experimental.** `Agents.jobs(agentId).register({ versionId, jobId })` registers job `jobId`, defined in the staged version `versionId`, on the target. It is available in a deployed agent only once the platform serves `jobs.register`, and needs the `supervision:register-job` grant (given per supervisor and target by an org owner or admin, scoped to jobs defined in the staged `versionId`). Without it the call is refused with `supervision_not_granted`. Under `lua test` / `lua dev` it is always refused with `supervision_release_offline_refused`, with no network call. An empty `versionId` or `jobId` is refused with `supervision_bad_args`, and an empty agent id with `supervision_no_target`.
|
|
85
|
+
|
|
84
86
|
- A workflow deploy also records a new active agent version. Read `versions.list` again before a later `promote`.
|
|
85
87
|
- `commitHash` on `workflows.deploy` is optional and forwarded unchanged. A call without it behaves as before, and the platform may fill it from the Job workspace.
|
|
86
88
|
|
|
@@ -115,7 +117,7 @@ await Agents.versions('base_agent-tp').delete(5);
|
|
|
115
117
|
### Offline (`lua test`, `lua dev`)
|
|
116
118
|
|
|
117
119
|
- **Read members** call the developer API with **your** credential and mirror the projections above: env keys only, and logs without user ids or message bodies.
|
|
118
|
-
- **Refused offline:** `versions.promote`, `persona.activate`, `versions.delete`, `workflows.create`, `workflows.push`, `workflows.deploy`, `versions.preview` and `data`. A local test never changes a live agent, and readable collections live on the grant.
|
|
120
|
+
- **Refused offline:** `versions.promote`, `persona.activate`, `versions.delete`, `workflows.create`, `workflows.push`, `workflows.deploy`, `jobs.register` (experimental), `versions.preview` and `data`. A local test never changes a live agent, and readable collections live on the grant.
|
|
119
121
|
- **Not available offline:** `Agents.list`, `Agents.get`, `source.get` and `Agents.deployed` (`supervision_offline_unsupported`).
|
|
120
122
|
|
|
121
123
|
### Errors
|
|
@@ -123,6 +125,9 @@ await Agents.versions('base_agent-tp').delete(5);
|
|
|
123
125
|
Every error is a `SupervisionError` with a `code`:
|
|
124
126
|
|
|
125
127
|
- `supervision_not_granted`
|
|
128
|
+
- `supervision_release_offline_refused`, for a release op (or the experimental `jobs.register`) run offline
|
|
129
|
+
- `supervision_bad_args`, a local check of the experimental `jobs.register`: an empty `versionId` or `jobId`
|
|
130
|
+
- `supervision_no_target`, a local check: no agent id is given
|
|
126
131
|
- `supervision_not_found`, for another org or an unknown agent
|
|
127
132
|
- `supervision_self_refused`
|
|
128
133
|
- `supervision_release_binding_required` and `supervision_release_binding_refused`
|
|
@@ -14,6 +14,8 @@ _Source of truth: workflows-spec 05 §5.17 (D19-r1/r2). This page is the develop
|
|
|
14
14
|
|
|
15
15
|
**Suspension does not release the workspace.** When your run waits for a person or a signal, its volume is left exactly as it was — it keeps holding one of the org's `maxWorkspacesPerOrg` slots until a terminal reason releases it, or, for a kept `empty` workspace, `ttlHours` expires (168 h at most once the ceiling is enabled). Releasing a workspace at suspension is not implemented yet (tracked as SA-134); until then, commit before a step suspends so your work is durable on the run branch `lua/wf-<lineageId>` independent of the volume.
|
|
16
16
|
|
|
17
|
+
**Storage is metered.** A Job-tier workspace volume, and every worktree-arm clone volume, is metered as GiB-hours (`sizeGb` × hours held, summed over the clones) on a `workflow:storage` usage record written when the volume is deleted; a held volume counts until its TTL expiry. The record is attributed to your organization and is for visibility only: it does not charge credits or touch the wallet, and whether storage is billed is a separate platform decision.
|
|
18
|
+
|
|
17
19
|
**Worktree arms:** parallel arms never share a filesystem; each gets its own clone and branch and the `merge` step integrates them — put `verify` on the workspace so an agent-resolved conflict must pass your tests. `onConflict:'agent'` lets one resolver turn fix conflicts.
|
|
18
20
|
|
|
19
21
|
## Commands from a code step — `ctx.exec` / `ctx.$`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lua-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.46.0",
|
|
4
4
|
"description": "Build, test, and deploy AI agents with custom tools, webhooks, and scheduled jobs. Features LuaAgent unified configuration, streaming chat, and batch deployment.",
|
|
5
5
|
"readmeFilename": "README.md",
|
|
6
6
|
"main": "dist/api-exports.js",
|
package/template/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { Button } from '@lua-ai-global/ui/button';
|
|
3
3
|
import { Card } from '@lua-ai-global/ui/card';
|
|
4
|
-
import { lua } from '
|
|
4
|
+
import { lua } from '@lua-ai-global/app-client';
|
|
5
5
|
|
|
6
6
|
export function App() {
|
|
7
7
|
const [message, setMessage] = useState<string>();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
3
|
import { App } from './App';
|
|
4
|
-
import { lua } from '
|
|
4
|
+
import { lua } from '@lua-ai-global/app-client';
|
|
5
5
|
import './index.css';
|
|
6
6
|
|
|
7
7
|
// Theme follows the shell: first from the bootstrap, then from the host's `init`.
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lua app client — the page side of a Lua web app.
|
|
3
|
-
*
|
|
4
|
-
* - Session: the shell opens this app in an iframe with a single-use handoff
|
|
5
|
-
* code in the URL fragment (`#code=…`). The client exchanges it with lua-auth
|
|
6
|
-
* for a Firebase custom token, signs in (identitytoolkit) and keeps the ID
|
|
7
|
-
* token in memory only, refreshing it (securetoken) before it expires. No
|
|
8
|
-
* cookie, no localStorage: a reload asks the shell for a new code.
|
|
9
|
-
* - Bridge (protocol v1): posts `lua:ready` once loaded, takes `lua:init`
|
|
10
|
-
* (theme, density, locale, path) from the host, posts `lua:auth:expired`
|
|
11
|
-
* when the session cannot be renewed so the shell re-issues a code. Messages
|
|
12
|
-
* go only to, and are only accepted from, the shells the gateway lists in
|
|
13
|
-
* the bootstrap (`hostOrigins`, the same list as the CSP's frame-ancestors).
|
|
14
|
-
* - `api(method, path, body?)`: calls a route of this app through the gateway
|
|
15
|
-
* (`/_lua/api/<path>`) with the session. A 401 renews the token once and
|
|
16
|
-
* retries; only a second 401 ends the session.
|
|
17
|
-
*
|
|
18
|
-
* Under `lua apps dev` the bootstrap says `dev: true`: there is no handoff and
|
|
19
|
-
* the dev server runs routes as the developer.
|
|
20
|
-
*
|
|
21
|
-
* Generated by `lua apps new`; yours to edit. (A packaged `@lua-ai-global/app-client`
|
|
22
|
-
* replaces this file later.)
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
export interface LuaBootstrap {
|
|
26
|
-
v: 1;
|
|
27
|
-
app: { id: string; name: string; version: string; deployment: number; origin: string };
|
|
28
|
-
apiBase: string;
|
|
29
|
-
manifest: string;
|
|
30
|
-
auth: { origin: string; exchangePath: string; audience: string; firebaseApiKey?: string };
|
|
31
|
-
embedded: boolean;
|
|
32
|
-
/** The shells allowed to frame this app; the bridge talks to these only. */
|
|
33
|
-
hostOrigins?: string[];
|
|
34
|
-
theme: 'light' | 'dark';
|
|
35
|
-
density: 'compact' | 'comfortable';
|
|
36
|
-
preview: boolean;
|
|
37
|
-
/** Set by `lua apps dev` only. */
|
|
38
|
-
dev?: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface LuaInit {
|
|
42
|
-
v: 1;
|
|
43
|
-
theme?: 'light' | 'dark';
|
|
44
|
-
density?: 'compact' | 'comfortable';
|
|
45
|
-
locale?: string;
|
|
46
|
-
path?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
declare global {
|
|
50
|
-
interface Window {
|
|
51
|
-
__LUA__?: LuaBootstrap;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const BRIDGE_VERSION = 1;
|
|
56
|
-
const IDENTITY = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken';
|
|
57
|
-
const SECURE_TOKEN = 'https://securetoken.googleapis.com/v1/token';
|
|
58
|
-
/** Renew this long before the ID token expires. */
|
|
59
|
-
const REFRESH_MARGIN_MS = 5 * 60_000;
|
|
60
|
-
|
|
61
|
-
export class LuaApiError extends Error {
|
|
62
|
-
constructor(
|
|
63
|
-
readonly status: number,
|
|
64
|
-
readonly code: string | undefined,
|
|
65
|
-
message: string,
|
|
66
|
-
readonly executionId?: string
|
|
67
|
-
) {
|
|
68
|
-
super(message);
|
|
69
|
-
this.name = 'LuaApiError';
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface Tokens {
|
|
74
|
-
idToken: string;
|
|
75
|
-
refreshToken: string;
|
|
76
|
-
expiresInS: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const nonEmpty = (v: unknown): v is string => typeof v === 'string' && v.length > 0;
|
|
80
|
-
|
|
81
|
-
/** A token response is only a session when every field is there; anything else is "no session". */
|
|
82
|
-
function tokensFrom(idToken: unknown, refreshToken: unknown, expiresIn: unknown): Tokens | null {
|
|
83
|
-
const expiresInS = Number(expiresIn);
|
|
84
|
-
if (!nonEmpty(idToken) || !nonEmpty(refreshToken) || !Number.isFinite(expiresInS) || expiresInS <= 0) return null;
|
|
85
|
-
return { idToken, refreshToken, expiresInS };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function jsonOrNull(response: Response | undefined): Promise<Record<string, unknown> | null> {
|
|
89
|
-
if (!response?.ok) return null;
|
|
90
|
-
try {
|
|
91
|
-
const body = await response.json();
|
|
92
|
-
return body && typeof body === 'object' ? (body as Record<string, unknown>) : null;
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
class LuaAppClient {
|
|
99
|
-
private readonly boot: LuaBootstrap | undefined = window.__LUA__;
|
|
100
|
-
private idToken: string | undefined;
|
|
101
|
-
private refreshToken: string | undefined;
|
|
102
|
-
private refreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
103
|
-
private renewing: Promise<boolean> | undefined;
|
|
104
|
-
private session: Promise<void> | undefined;
|
|
105
|
-
private readonly initListeners: Array<(init: LuaInit) => void> = [];
|
|
106
|
-
|
|
107
|
-
get appName(): string {
|
|
108
|
-
return this.boot?.app.name ?? document.title;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
get theme(): 'light' | 'dark' {
|
|
112
|
-
return this.boot?.theme ?? 'dark';
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
onInit(listener: (init: LuaInit) => void): void {
|
|
116
|
-
this.initListeners.push(listener);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Take the handoff code, open the session, tell the host we are ready. */
|
|
120
|
-
start(): Promise<void> {
|
|
121
|
-
if (!this.session) {
|
|
122
|
-
window.addEventListener('message', (event) => {
|
|
123
|
-
// Only the shell that framed us, from an origin the gateway allows, speaks the bridge.
|
|
124
|
-
if (event.source !== window.parent || !this.hostOrigins().includes(event.origin)) return;
|
|
125
|
-
if (event.data?.type !== 'lua:init') return;
|
|
126
|
-
for (const listener of this.initListeners) listener(event.data as LuaInit);
|
|
127
|
-
});
|
|
128
|
-
this.session = this.openSession().finally(() => this.post({ type: 'lua:ready', v: BRIDGE_VERSION }));
|
|
129
|
-
}
|
|
130
|
-
return this.session;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async api<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
134
|
-
await this.start();
|
|
135
|
-
let response = await this.send(method, path, body);
|
|
136
|
-
// A 401 usually means the ID token lapsed between refreshes: renew once and retry once.
|
|
137
|
-
if (response.status === 401 && (await this.renew())) response = await this.send(method, path, body);
|
|
138
|
-
if (response.status === 401) this.expired();
|
|
139
|
-
const text = await response.text();
|
|
140
|
-
let data: any;
|
|
141
|
-
try {
|
|
142
|
-
data = text ? JSON.parse(text) : undefined;
|
|
143
|
-
} catch {
|
|
144
|
-
data = text;
|
|
145
|
-
}
|
|
146
|
-
if (!response.ok) {
|
|
147
|
-
throw new LuaApiError(
|
|
148
|
-
response.status,
|
|
149
|
-
data?.code,
|
|
150
|
-
data?.message ?? response.statusText,
|
|
151
|
-
response.headers.get('x-lua-execution-id') ?? undefined
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
return data as T;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private send(method: string, path: string, body?: unknown): Promise<Response> {
|
|
158
|
-
const base = this.boot?.apiBase ?? '/_lua/api';
|
|
159
|
-
const headers: Record<string, string> = { accept: 'application/json' };
|
|
160
|
-
if (this.idToken) headers.authorization = `Bearer ${this.idToken}`;
|
|
161
|
-
if (body !== undefined) headers['content-type'] = 'application/json';
|
|
162
|
-
return fetch(`${base}${path.startsWith('/') ? path : `/${path}`}`, {
|
|
163
|
-
method,
|
|
164
|
-
headers,
|
|
165
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private async openSession(): Promise<void> {
|
|
170
|
-
const boot = this.boot;
|
|
171
|
-
if (!boot || boot.dev) return; // `lua apps dev`: the dev server is the caller's session.
|
|
172
|
-
const code = new URLSearchParams(location.hash.slice(1)).get('code');
|
|
173
|
-
// The code is single-use: drop it from the URL before anything can copy it.
|
|
174
|
-
if (code) history.replaceState(null, '', location.pathname + location.search);
|
|
175
|
-
if (!code || !boot.auth.firebaseApiKey) return this.expired();
|
|
176
|
-
|
|
177
|
-
const exchanged = await jsonOrNull(
|
|
178
|
-
await fetch(`${boot.auth.origin}${boot.auth.exchangePath}`, {
|
|
179
|
-
method: 'POST',
|
|
180
|
-
headers: { 'content-type': 'application/json' },
|
|
181
|
-
body: JSON.stringify({ code, audience: boot.auth.audience }),
|
|
182
|
-
}).catch(() => undefined)
|
|
183
|
-
);
|
|
184
|
-
if (!nonEmpty(exchanged?.token)) return this.expired();
|
|
185
|
-
|
|
186
|
-
const signedIn = await jsonOrNull(
|
|
187
|
-
await fetch(`${IDENTITY}?key=${encodeURIComponent(boot.auth.firebaseApiKey)}`, {
|
|
188
|
-
method: 'POST',
|
|
189
|
-
headers: { 'content-type': 'application/json' },
|
|
190
|
-
body: JSON.stringify({ token: exchanged!.token, returnSecureToken: true }),
|
|
191
|
-
}).catch(() => undefined)
|
|
192
|
-
);
|
|
193
|
-
const tokens = signedIn && tokensFrom(signedIn.idToken, signedIn.refreshToken, signedIn.expiresIn);
|
|
194
|
-
if (!tokens) return this.expired();
|
|
195
|
-
this.keep(tokens);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
private keep(tokens: Tokens): void {
|
|
199
|
-
this.idToken = tokens.idToken;
|
|
200
|
-
this.refreshToken = tokens.refreshToken;
|
|
201
|
-
clearTimeout(this.refreshTimer);
|
|
202
|
-
this.refreshTimer = setTimeout(
|
|
203
|
-
() => void this.renew().then((ok) => ok || this.expired()),
|
|
204
|
-
Math.max(tokens.expiresInS * 1000 - REFRESH_MARGIN_MS, 30_000)
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Swap the refresh token for a new ID token. One renewal at a time; `false` when it cannot be done. */
|
|
209
|
-
private renew(): Promise<boolean> {
|
|
210
|
-
if (!this.renewing) {
|
|
211
|
-
this.renewing = this.refreshOnce().finally(() => (this.renewing = undefined));
|
|
212
|
-
}
|
|
213
|
-
return this.renewing;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private async refreshOnce(): Promise<boolean> {
|
|
217
|
-
const key = this.boot?.auth.firebaseApiKey;
|
|
218
|
-
if (!key || !this.refreshToken) return false;
|
|
219
|
-
const r = await jsonOrNull(
|
|
220
|
-
await fetch(`${SECURE_TOKEN}?key=${encodeURIComponent(key)}`, {
|
|
221
|
-
method: 'POST',
|
|
222
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
223
|
-
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: this.refreshToken }),
|
|
224
|
-
}).catch(() => undefined)
|
|
225
|
-
);
|
|
226
|
-
const tokens = r && tokensFrom(r.id_token, r.refresh_token, r.expires_in);
|
|
227
|
-
if (!tokens) return false;
|
|
228
|
-
this.keep(tokens);
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** The session is gone: the shell answers with a fresh handoff code. */
|
|
233
|
-
private expired(): void {
|
|
234
|
-
this.idToken = undefined;
|
|
235
|
-
this.refreshToken = undefined;
|
|
236
|
-
clearTimeout(this.refreshTimer);
|
|
237
|
-
this.post({ type: 'lua:auth:expired', v: BRIDGE_VERSION });
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private hostOrigins(): string[] {
|
|
241
|
-
return this.boot?.hostOrigins ?? [];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* One post per allowed shell origin: the browser delivers it only when the
|
|
246
|
-
* framing window really has that origin, so no other page ever receives it.
|
|
247
|
-
*
|
|
248
|
-
* A shell on a custom scheme (lua-desktop's `tauri://localhost`) is not a
|
|
249
|
-
* valid postMessage target origin: WebKit throws "Invalid target origin",
|
|
250
|
-
* which aborted the loop and the session ("Could not load" in lua-desktop),
|
|
251
|
-
* and skipping it would leave the desktop shell without `lua:ready`.
|
|
252
|
-
* Such an origin is posted with '*': the messages carry only {type, v}, and
|
|
253
|
-
* the CSP's frame-ancestors already limits who can frame the app. Each post
|
|
254
|
-
* is isolated so one origin can never stop the others.
|
|
255
|
-
*/
|
|
256
|
-
private post(message: { type: string; v: number }): void {
|
|
257
|
-
if (window.parent === window) return;
|
|
258
|
-
const targets = new Set(this.hostOrigins().map((origin) => (/^https?:\/\//i.test(origin) ? origin : '*')));
|
|
259
|
-
for (const target of targets) {
|
|
260
|
-
try {
|
|
261
|
-
window.parent.postMessage(message, target);
|
|
262
|
-
} catch {
|
|
263
|
-
// An origin this browser will not target: skip it, keep the rest.
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export const lua = new LuaAppClient();
|