buckyos 0.7.159 → 0.7.160
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/cli/dist/cli.mjs +396 -18
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/modules/aiws.ts +327 -4
- package/cli/modules/pikg.ts +29 -3
- package/cli/modules/pikg_protocol.ts +42 -3
- package/cli/modules/share.ts +2 -1
- package/cli/version.ts +1 -1
- package/dist/aiworkspace_client.d.ts +143 -0
- package/dist/aiworkspace_client.d.ts.map +1 -1
- package/dist/aiws_ext.cjs +2 -0
- package/dist/aiws_ext.cjs.map +1 -0
- package/dist/aiws_ext.d.ts +169 -0
- package/dist/aiws_ext.d.ts.map +1 -0
- package/dist/aiws_ext.mjs +499 -0
- package/dist/aiws_ext.mjs.map +1 -0
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +2 -2
- package/dist/{executor_client-24abb248.js → executor_client-47edd42d.js} +2 -2
- package/dist/executor_client-47edd42d.js.map +1 -0
- package/dist/{executor_client-24076936.mjs → executor_client-50023bcc.mjs} +58 -1
- package/dist/executor_client-50023bcc.mjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +36 -2
- package/dist/index.mjs.map +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.mjs +38 -2
- package/dist/node.mjs.map +1 -1
- package/package.json +6 -1
- package/dist/executor_client-24076936.mjs.map +0 -1
- package/dist/executor_client-24abb248.js.map +0 -1
package/cli/modules/aiws.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AiwsSourceError,
|
|
3
|
+
buildControl,
|
|
4
|
+
exportBundle,
|
|
5
|
+
NfspClient,
|
|
6
|
+
pullControl,
|
|
7
|
+
watchControl,
|
|
8
|
+
} from 'buckyos/node'
|
|
1
9
|
import type { CommandDefinition, CommandModule, JsonSchema, OptionDefinition, PositionalDefinition } from '../core/command.ts'
|
|
2
10
|
import type { CommandContext } from '../core/context.ts'
|
|
3
|
-
import { ToolError, UsageError } from '../core/errors.ts'
|
|
11
|
+
import { EXIT_AUTH, ToolError, UsageError } from '../core/errors.ts'
|
|
4
12
|
import { callService, expectObject, inputString, requiredInputString } from '../core/service.ts'
|
|
5
13
|
import { getHost } from '../runtime/host.ts'
|
|
14
|
+
import { createPikgModule } from './pikg.ts'
|
|
15
|
+
import { gatewayOrigin } from './share.ts'
|
|
6
16
|
|
|
7
17
|
// AI Workspace (`/kapi/aiworkspace`): reading a workspace, and the executor protocol of a wish run
|
|
8
18
|
// (内核架构改进 M3): claim → heartbeat → report. Source of truth for the methods:
|
|
@@ -23,7 +33,11 @@ async function aiws(ctx: CommandContext, method: string, params: Record<string,
|
|
|
23
33
|
const error = expectObject(result.error, `${method}.error`)
|
|
24
34
|
const code = typeof error.code === 'string' ? error.code : 'AIWS_ERROR'
|
|
25
35
|
const detail = typeof error.detail === 'string' ? error.detail : code
|
|
26
|
-
|
|
36
|
+
// the structured part (a build's diagnostics, a conflict's revisions) travels in the error's details
|
|
37
|
+
const details: Record<string, unknown> = {}
|
|
38
|
+
if (typeof error.sub_code === 'string') details.sub_code = error.sub_code
|
|
39
|
+
if (error.data !== undefined) details.data = error.data
|
|
40
|
+
throw new ToolError(code, `${code}${typeof error.sub_code === 'string' ? `/${error.sub_code}` : ''}: ${detail}`, undefined, false, details)
|
|
27
41
|
}
|
|
28
42
|
return result
|
|
29
43
|
}
|
|
@@ -375,10 +389,319 @@ const exportCommand: CommandDefinition = {
|
|
|
375
389
|
},
|
|
376
390
|
}
|
|
377
391
|
|
|
392
|
+
// ---- extension controls (buckyos aiworkspace README “Extension controls”): source in the Zone's home folders,
|
|
393
|
+
// built into a Workspace, exported as a bundle / `aiws` PIKG, installed into the caller's library, imported elsewhere.
|
|
394
|
+
// The drivers are buckyos-websdk src/aiws_ext.ts, shared with the Desktop.
|
|
395
|
+
|
|
396
|
+
async function sessionToken(ctx: CommandContext): Promise<string> {
|
|
397
|
+
const session = ctx.session
|
|
398
|
+
if (!session) throw new ToolError('AUTH_REQUIRED', 'authenticated session is required', EXIT_AUTH)
|
|
399
|
+
return (await session.ensureValid()).token
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The Zone file service (NFSP) with the caller's session: where control sources live. */
|
|
403
|
+
async function openHome(ctx: CommandContext): Promise<NfspClient> {
|
|
404
|
+
const session = ctx.session
|
|
405
|
+
if (!session) throw new ToolError('AUTH_REQUIRED', 'authenticated session is required', EXIT_AUTH)
|
|
406
|
+
const nfsp = new NfspClient({ baseUrl: gatewayOrigin(ctx.connection.endpoint), sessionToken: async () => (await session.ensureValid()).token })
|
|
407
|
+
await nfsp.hello(['cli'])
|
|
408
|
+
return nfsp
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The two calls the shared drivers make (`AiwsExtRpc` of buckyos-websdk src/aiws_ext.ts). */
|
|
412
|
+
function extRpc(ctx: CommandContext) {
|
|
413
|
+
return {
|
|
414
|
+
extBuild: (workspace_id: string, params: Record<string, unknown>) => aiws(ctx, 'ext.build', { workspace_id, ...params }) as never,
|
|
415
|
+
extExport: (workspace_id: string, entity_ids: string[], purpose: string = 'package') => aiws(ctx, 'ext.export', { workspace_id, entity_ids, purpose }) as never,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Bytes of a Workspace object (`GET /kapi/aiworkspace/asset/<ws>/<obj_id>`, authorized through what references it). */
|
|
420
|
+
function assetFetcher(ctx: CommandContext): (workspaceId: string, objId: string) => Promise<Uint8Array> {
|
|
421
|
+
return async (workspaceId, objId) => {
|
|
422
|
+
const response = await fetch(`${gatewayOrigin(ctx.connection.endpoint)}/kapi/aiworkspace/asset/${encodeURIComponent(workspaceId)}/${encodeURIComponent(objId)}`, {
|
|
423
|
+
headers: { Authorization: `Bearer ${await sessionToken(ctx)}` },
|
|
424
|
+
signal: ctx.signal,
|
|
425
|
+
})
|
|
426
|
+
if (!response.ok) throw new ToolError('AIWS_ASSET_UNAVAILABLE', `asset ${objId}: HTTP ${response.status}`)
|
|
427
|
+
return new Uint8Array(await response.arrayBuffer())
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Source problems (not refusals of the service) as a build failure with diagnostics. */
|
|
432
|
+
async function sourceErrors<T>(run: () => Promise<T>): Promise<T> {
|
|
433
|
+
try {
|
|
434
|
+
return await run()
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (error instanceof AiwsSourceError) throw new ToolError('BUILD_FAILED', `BUILD_FAILED: ${error.message}`, undefined, false, { data: { diagnostics: error.diagnostics } })
|
|
437
|
+
throw error
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function list(input: Record<string, unknown>, key: string): string[] {
|
|
442
|
+
return requiredInputString(input, key).split(',').map((s) => s.trim()).filter(Boolean)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---- local source folders: a developer's or an Agent's own disk (files go inline, ≤ 4 MiB per build)
|
|
446
|
+
|
|
447
|
+
const LOCAL_STATE = '.aiws-build.json'
|
|
448
|
+
|
|
449
|
+
async function isLocalDir(path: string): Promise<boolean> {
|
|
450
|
+
try { return (await getHost().stat(path)).isDirectory } catch { return false }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The files of a local (flat) source folder as `ext.build` takes them inline. */
|
|
454
|
+
async function localFiles(dir: string): Promise<{ name: string; base64: string }[]> {
|
|
455
|
+
const host = getHost()
|
|
456
|
+
const files: { name: string; base64: string }[] = []
|
|
457
|
+
const problems: { file: string; severity: 'error'; message: string }[] = []
|
|
458
|
+
let total = 0
|
|
459
|
+
for (const entry of await host.readDir(dir)) {
|
|
460
|
+
if (entry.name.startsWith('.')) continue
|
|
461
|
+
if (!entry.isFile) { problems.push({ file: entry.name, severity: 'error', message: 'a control source folder is flat: move files out of sub-folders' }); continue }
|
|
462
|
+
const bytes = await host.readFile(host.path.join(dir, entry.name))
|
|
463
|
+
total += bytes.length
|
|
464
|
+
files.push({ name: entry.name, base64: btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join('')) })
|
|
465
|
+
}
|
|
466
|
+
if (files.length === 0) problems.push({ file: '', severity: 'error', message: `${dir} holds no files` })
|
|
467
|
+
if (total > 4 * 1024 * 1024) problems.push({ file: '', severity: 'error', message: 'a control is limited to 4 MiB per build from a local folder' })
|
|
468
|
+
if (problems.length > 0) throw new ToolError('BUILD_FAILED', `BUILD_FAILED: ${problems.map((p) => `${p.file}: ${p.message}`).join('; ')}`, undefined, false, { data: { diagnostics: problems } })
|
|
469
|
+
return files.sort((a, b) => a.name.localeCompare(b.name))
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function localState(dir: string): Promise<Record<string, unknown> | null> {
|
|
473
|
+
try { return JSON.parse(await getHost().readTextFile(getHost().path.join(dir, LOCAL_STATE))) as Record<string, unknown> } catch { return null }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Build a local folder into a Workspace; the base it was built on is kept in its `.aiws-build.json`. */
|
|
477
|
+
async function buildLocal(ctx: CommandContext, ws: string, dir: string, opts: { folderId?: string; force?: boolean; dryRun?: boolean }): Promise<Record<string, unknown>> {
|
|
478
|
+
const state = await localState(dir)
|
|
479
|
+
const result = await aiws(ctx, 'ext.build', {
|
|
480
|
+
workspace_id: ws, files: await localFiles(dir),
|
|
481
|
+
...(opts.folderId ? { folder_id: opts.folderId } : {}),
|
|
482
|
+
...(state && state.workspace_id === ws ? { expect: { def_rev: state.def_rev, dir_rev: state.dir_rev } } : {}),
|
|
483
|
+
...(opts.force ? { force: true } : {}), ...(opts.dryRun ? { dry_run: true } : {}),
|
|
484
|
+
})
|
|
485
|
+
if (!opts.dryRun && typeof result.def_entity_id === 'string' && typeof result.def_rev === 'number') {
|
|
486
|
+
await getHost().writeTextFile(getHost().path.join(dir, LOCAL_STATE), `${JSON.stringify({ workspace_id: ws, def_entity_id: result.def_entity_id, def_rev: result.def_rev, dir_rev: result.dir_rev ?? 0, ident: result.ident, at: new Date().toISOString() }, null, 2)}\n`)
|
|
487
|
+
}
|
|
488
|
+
return result
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const buildCommand: CommandDefinition = {
|
|
492
|
+
verb: 'build',
|
|
493
|
+
summary: 'Build a control from its source folder (local, or in the Zone; component.json + files) into a Workspace; instances update, a failure commits nothing',
|
|
494
|
+
description: 'The folder is a local folder (when it exists here; its files are sent inline, ≤ 4 MiB) or a Zone path (e.g. /home/alice/AI Workspace/Demo/controls/kpi-card; --zone-path forces it), flat. The base of the last build or pull is kept in its .aiws-build.json; building over a newer Workspace version is refused (pull first, or --force). --watch rebuilds on every change until interrupted.',
|
|
495
|
+
positionals: [{ name: 'dir', description: 'Local folder, or Zone path of the control source folder', required: true }],
|
|
496
|
+
options: [
|
|
497
|
+
WORKSPACE_OPTION,
|
|
498
|
+
{ name: 'folder', property: 'folder_id', description: 'Data folder for the definition (default: data/extensions)', type: 'string' },
|
|
499
|
+
{ name: 'watch', property: 'watch', description: 'Rebuild whenever the folder changes', type: 'boolean' },
|
|
500
|
+
{ name: 'dry-run', property: 'dry_run', description: 'Check only: diagnostics, nothing committed', type: 'boolean' },
|
|
501
|
+
{ name: 'force', property: 'force', description: 'Build over whatever the Workspace holds now', type: 'boolean' },
|
|
502
|
+
{ name: 'zone-path', property: 'zone_path', description: 'Treat the folder as a Zone path even if a local folder has that name', type: 'boolean' },
|
|
503
|
+
],
|
|
504
|
+
inputSchema: workspaceInput({ dir: { type: 'string', minLength: 1 }, folder_id: { type: 'string' }, watch: { type: 'boolean' }, dry_run: { type: 'boolean' }, force: { type: 'boolean' }, zone_path: { type: 'boolean' } }, ['dir']),
|
|
505
|
+
outputSchema: OBJECT_OUTPUT,
|
|
506
|
+
resultSchemaVersion: 1,
|
|
507
|
+
access: { mode: 'fixed', level: 'write' },
|
|
508
|
+
asyncMode: 'sync',
|
|
509
|
+
requiresSession: true,
|
|
510
|
+
examples: ['buckyos aiws build "/home/alice/AI Workspace/Demo/controls/kpi-card" --workspace ws_abc', 'buckyos aiws build "/home/alice/AI Workspace/Demo/controls/kpi-card" --workspace ws_abc --watch'],
|
|
511
|
+
handler: async (ctx, input) => {
|
|
512
|
+
const ws = requiredInputString(input, 'workspace_id')
|
|
513
|
+
const dir = requiredInputString(input, 'dir')
|
|
514
|
+
const opts = { folderId: inputString(input, 'folder_id'), force: input.force === true, dryRun: input.dry_run === true }
|
|
515
|
+
if (input.zone_path !== true && await isLocalDir(getHost().path.resolve(ctx.cwd, dir))) {
|
|
516
|
+
const local = getHost().path.resolve(ctx.cwd, dir)
|
|
517
|
+
if (input.watch === true) throw new UsageError('AIWS_USAGE', '--watch follows a Zone folder; rebuild a local folder yourself')
|
|
518
|
+
return buildLocal(ctx, ws, local, opts)
|
|
519
|
+
}
|
|
520
|
+
const nfsp = await openHome(ctx)
|
|
521
|
+
if (input.watch !== true) return sourceErrors(() => buildControl(extRpc(ctx), nfsp, ws, dir, opts))
|
|
522
|
+
let builds = 0
|
|
523
|
+
let failures = 0
|
|
524
|
+
await watchControl(nfsp, dir, async () => {
|
|
525
|
+
try {
|
|
526
|
+
const r = await sourceErrors(() => buildControl(extRpc(ctx), nfsp, ws, dir, opts))
|
|
527
|
+
builds++
|
|
528
|
+
await ctx.io.stderr(`built ${r.ident} v${r.version ?? '?'} (${r.unchanged ? 'unchanged' : r.status})\n`)
|
|
529
|
+
} catch (error) {
|
|
530
|
+
failures++
|
|
531
|
+
const diagnostics = error instanceof ToolError ? (error.details.data as { diagnostics?: unknown[] } | undefined)?.diagnostics : undefined
|
|
532
|
+
await ctx.io.stderr(`build failed: ${error instanceof Error ? error.message : String(error)}\n${diagnostics ? `${JSON.stringify(diagnostics, null, 2)}\n` : ''}`)
|
|
533
|
+
}
|
|
534
|
+
}, { signal: ctx.signal })
|
|
535
|
+
return { ok: true, watched: dir, builds, failures }
|
|
536
|
+
},
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const pullCommand: CommandDefinition = {
|
|
540
|
+
verb: 'pull',
|
|
541
|
+
summary: 'Write a control definition’s Workspace snapshot into a folder — a Zone path (default) or a local one (--local) — as the base of the next build',
|
|
542
|
+
positionals: [{ name: 'def', description: 'Definition (buckyos.block-def) id', required: true }],
|
|
543
|
+
options: [WORKSPACE_OPTION, { name: 'out', property: 'out', description: 'Folder to write (created)', type: 'string', required: true }, { name: 'local', property: 'local', description: 'Write into a local folder instead of the Zone', type: 'boolean' }],
|
|
544
|
+
inputSchema: workspaceInput({ def: { type: 'string', minLength: 1 }, out: { type: 'string', minLength: 1 }, local: { type: 'boolean' } }, ['def', 'out']),
|
|
545
|
+
outputSchema: OBJECT_OUTPUT,
|
|
546
|
+
resultSchemaVersion: 1,
|
|
547
|
+
access: { mode: 'fixed', level: 'write' },
|
|
548
|
+
asyncMode: 'sync',
|
|
549
|
+
requiresSession: true,
|
|
550
|
+
examples: ['buckyos aiws pull def_abc --workspace ws_abc --out "/home/alice/AI Workspace/Demo/controls/kpi-card"'],
|
|
551
|
+
handler: async (ctx, input) => {
|
|
552
|
+
if (input.local === true) {
|
|
553
|
+
const ws = requiredInputString(input, 'workspace_id')
|
|
554
|
+
const host = getHost()
|
|
555
|
+
const out = host.path.resolve(ctx.cwd, requiredInputString(input, 'out'))
|
|
556
|
+
const r = await aiws(ctx, 'ext.export', { workspace_id: ws, entity_ids: [requiredInputString(input, 'def')], purpose: 'pull' })
|
|
557
|
+
const control = expectObject((r.controls as unknown[])[0], 'ext.export.controls[0]')
|
|
558
|
+
await host.mkdir(out, { recursive: true })
|
|
559
|
+
const fetchAsset = assetFetcher(ctx)
|
|
560
|
+
const files: string[] = []
|
|
561
|
+
for (const f of control.files as { name: string; obj_id?: string; text?: string }[]) {
|
|
562
|
+
const target = host.path.join(out, f.name)
|
|
563
|
+
if (f.text !== undefined) await host.writeTextFile(target, f.text)
|
|
564
|
+
else await host.writeFile(target, await fetchAsset(ws, String(f.obj_id)))
|
|
565
|
+
files.push(f.name)
|
|
566
|
+
}
|
|
567
|
+
await host.writeTextFile(host.path.join(out, LOCAL_STATE), `${JSON.stringify({ workspace_id: ws, def_entity_id: control.entity_id, def_rev: control.def_rev, dir_rev: control.dir_rev, ident: control.ident, at: new Date().toISOString() }, null, 2)}\n`)
|
|
568
|
+
return { ok: true, out, files }
|
|
569
|
+
}
|
|
570
|
+
const nfsp = await openHome(ctx)
|
|
571
|
+
const r = await pullControl(extRpc(ctx), nfsp, requiredInputString(input, 'workspace_id'), requiredInputString(input, 'def'), requiredInputString(input, 'out'), assetFetcher(ctx))
|
|
572
|
+
return { ok: true, out: requiredInputString(input, 'out'), ...r }
|
|
573
|
+
},
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Run a verb of the pikg module (init / build / pack) with this command's context. */
|
|
577
|
+
async function pikgVerb(ctx: CommandContext, verb: string, input: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
578
|
+
const command = createPikgModule().commands.find((c) => c.verb === verb)
|
|
579
|
+
if (!command) throw new ToolError('INTERNAL', `pikg ${verb} is missing`)
|
|
580
|
+
return expectObject(await command.handler(ctx, input), `pikg ${verb}`)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Bind a local `.pikg` into the caller's own `user/aiworkspace/<user>/exports` bucket (the data plane PUT, ≤ 32 MiB):
|
|
584
|
+
* what `ext.install { pikg: { bucket_id, key } }` takes (and consumes once installed). */
|
|
585
|
+
async function uploadPackage(ctx: CommandContext, path: string): Promise<{ bucket_id: string; key: string }> {
|
|
586
|
+
const bucket_id = `user/aiworkspace/${ctx.principal.id}/exports`
|
|
587
|
+
const key = `aiws/${Date.now()}-${getHost().path.basename(path)}`
|
|
588
|
+
const bytes = await getHost().readFile(path)
|
|
589
|
+
if (bytes.byteLength > 32 * 1024 * 1024) throw new UsageError('FILE_TOO_LARGE', 'a control package installed from the CLI is limited to 32 MiB')
|
|
590
|
+
const url = `${gatewayOrigin(ctx.connection.endpoint)}/bucket/v1/${bucket_id}/${key.split('/').map(encodeURIComponent).join('/')}`
|
|
591
|
+
const response = await fetch(url, { method: 'PUT', headers: { Authorization: `Bearer ${await sessionToken(ctx)}` }, body: new Blob([bytes as Uint8Array<ArrayBuffer>]), signal: ctx.signal })
|
|
592
|
+
if (!response.ok) throw new ToolError('AIWS_UPLOAD_FAILED', `upload of ${path}: HTTP ${response.status} ${await response.text().catch(() => '')}`)
|
|
593
|
+
return { bucket_id, key }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const extExportCommand: CommandDefinition = {
|
|
597
|
+
verb: 'ext-export',
|
|
598
|
+
summary: 'Export Workspace controls as a bundle in a local folder; optionally pack it as an aiws PIKG and install it into your library',
|
|
599
|
+
description: 'Writes <out>/aiws/ (aiws-manifest.json + controls/<dir>/). --pikg (or --install) also runs pikg init/build/pack in <out> (kind aiws-controls); --install binds the .pikg into your aiworkspace exports bucket and calls ext.install.',
|
|
600
|
+
positionals: [{ name: 'defs', description: 'Definition (or instance) ids, comma-separated', required: true }],
|
|
601
|
+
options: [
|
|
602
|
+
WORKSPACE_OPTION,
|
|
603
|
+
{ name: 'out', property: 'out', description: 'Local folder to write into (created)', type: 'string', required: true },
|
|
604
|
+
{ name: 'name', property: 'name', description: 'Package name (default: the first control’s)', type: 'string' },
|
|
605
|
+
{ name: 'version', property: 'version', description: 'Package version (default 0.1.0)', type: 'string' },
|
|
606
|
+
{ name: 'owner', property: 'owner', description: 'Owner DID of the package (default did:bns:<you>)', type: 'string' },
|
|
607
|
+
{ name: 'pikg', property: 'pikg', description: 'Also pack the bundle as an aiws PIKG', type: 'boolean' },
|
|
608
|
+
{ name: 'install', property: 'install', description: 'Pack, then install into your AI Workspace library (dev mode)', type: 'boolean' },
|
|
609
|
+
],
|
|
610
|
+
inputSchema: workspaceInput({ defs: { type: 'string', minLength: 1 }, out: { type: 'string', minLength: 1 }, name: { type: 'string' }, version: { type: 'string' }, owner: { type: 'string' }, pikg: { type: 'boolean' }, install: { type: 'boolean' } }, ['defs', 'out']),
|
|
611
|
+
outputSchema: OBJECT_OUTPUT,
|
|
612
|
+
resultSchemaVersion: 1,
|
|
613
|
+
access: { mode: 'fixed', level: 'write' },
|
|
614
|
+
asyncMode: 'sync',
|
|
615
|
+
requiresSession: true,
|
|
616
|
+
examples: ['buckyos aiws ext-export def_abc,def_xyz --workspace ws_abc --out ./kpi-pack --install'],
|
|
617
|
+
handler: async (ctx, input) => {
|
|
618
|
+
const ws = requiredInputString(input, 'workspace_id')
|
|
619
|
+
const host = getHost()
|
|
620
|
+
const out = host.path.resolve(ctx.cwd, requiredInputString(input, 'out'))
|
|
621
|
+
const bundle = await exportBundle(extRpc(ctx), ws, list(input, 'defs'), assetFetcher(ctx))
|
|
622
|
+
const root = host.path.join(out, 'aiws')
|
|
623
|
+
// a previous export's controls must not linger in the bundle
|
|
624
|
+
await host.remove(root, { recursive: true }).catch(() => undefined)
|
|
625
|
+
for (const [path, bytes] of bundle.files) {
|
|
626
|
+
const target = host.path.join(root, path)
|
|
627
|
+
await host.mkdir(host.path.dirname(target), { recursive: true })
|
|
628
|
+
await host.writeFile(target, bytes)
|
|
629
|
+
}
|
|
630
|
+
const result: Record<string, unknown> = { ok: true, bundle: root, controls: bundle.manifest.controls.map((c: Record<string, unknown>) => ({ ident: c.ident, kind: c.kind, version: c.version, digest: c.digest })) }
|
|
631
|
+
if (input.pikg !== true && input.install !== true) return result
|
|
632
|
+
const name = inputString(input, 'name') ?? bundle.manifest.controls[0]?.name.replace(/[._]/g, '-') ?? 'controls'
|
|
633
|
+
const owner = inputString(input, 'owner') ?? `did:bns:${ctx.principal.id}`
|
|
634
|
+
const version = inputString(input, 'version') ?? '0.1.0'
|
|
635
|
+
const appJson = host.path.join(out, 'dapp_meta', 'app.json')
|
|
636
|
+
const existing = await host.readTextFile(appJson).then((t) => JSON.parse(t) as Record<string, unknown>).catch(() => null)
|
|
637
|
+
if (!existing) {
|
|
638
|
+
await pikgVerb(ctx, 'init', { project_dir: out, name, owner, kind: 'aiws-controls', source: './aiws', version })
|
|
639
|
+
} else if (inputString(input, 'version') && existing.version !== version) {
|
|
640
|
+
// a re-export at a new version: the same project, the new version (a library refuses other content at an old one)
|
|
641
|
+
await host.writeTextFile(appJson, `${JSON.stringify({ ...existing, version }, null, 2)}\n`)
|
|
642
|
+
const pikgJson = host.path.join(out, 'dapp_meta', 'pikg.json')
|
|
643
|
+
const meta = JSON.parse(await host.readTextFile(pikgJson)) as Record<string, unknown>
|
|
644
|
+
await host.writeTextFile(pikgJson, `${JSON.stringify({ ...meta, pikg_file: `${existing.name}-${version}.pikg` }, null, 2)}\n`)
|
|
645
|
+
}
|
|
646
|
+
await pikgVerb(ctx, 'build', { meta_dir: host.path.join(out, 'dapp_meta') })
|
|
647
|
+
const packed = await pikgVerb(ctx, 'pack', { dist_dir: host.path.join(out, 'dapp_dist') })
|
|
648
|
+
const pikgPath = String(packed.pikg_path)
|
|
649
|
+
result.pikg = pikgPath
|
|
650
|
+
if (input.install !== true) return result
|
|
651
|
+
result.install = await aiws(ctx, 'ext.install', { pikg: await uploadPackage(ctx, pikgPath), mode: 'dev' })
|
|
652
|
+
return result
|
|
653
|
+
},
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const libraryCommand: CommandDefinition = {
|
|
657
|
+
verb: 'library',
|
|
658
|
+
summary: 'Your AI Workspace control library: `list`, `install <file.pikg>` (an aiws package, dev mode), `remove <pkg_id>`',
|
|
659
|
+
positionals: [{ name: 'action', description: 'list | install | remove', required: true }, { name: 'target', description: 'install: local .pikg path; remove: package id', required: false }],
|
|
660
|
+
options: [],
|
|
661
|
+
inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'install', 'remove'] }, target: { type: 'string' } }, required: ['action'], additionalProperties: false },
|
|
662
|
+
outputSchema: OBJECT_OUTPUT,
|
|
663
|
+
resultSchemaVersion: 1,
|
|
664
|
+
access: { mode: 'fixed', level: 'write' },
|
|
665
|
+
asyncMode: 'sync',
|
|
666
|
+
requiresSession: true,
|
|
667
|
+
examples: ['buckyos aiws library list', 'buckyos aiws library install ./kpi-pack-0.1.0.pikg', 'buckyos aiws library remove kpi-pack.alice.bns.did'],
|
|
668
|
+
handler: async (ctx, input) => {
|
|
669
|
+
const action = requiredInputString(input, 'action')
|
|
670
|
+
if (action === 'list') return aiws(ctx, 'ext.list_library', {})
|
|
671
|
+
if (action === 'remove') return aiws(ctx, 'ext.uninstall', { pkg_id: requiredInputString(input, 'target') })
|
|
672
|
+
if (action !== 'install') throw new UsageError('AIWS_USAGE', 'library: list, install <file.pikg> or remove <pkg_id>')
|
|
673
|
+
const path = getHost().path.resolve(ctx.cwd, requiredInputString(input, 'target'))
|
|
674
|
+
return aiws(ctx, 'ext.install', { pikg: await uploadPackage(ctx, path), mode: 'dev' })
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const extImportCommand: CommandDefinition = {
|
|
679
|
+
verb: 'ext-import',
|
|
680
|
+
summary: 'Bring controls of your library into a Workspace (one Commit): <pkg_id>#<ident>, comma-separated',
|
|
681
|
+
positionals: [{ name: 'items', description: '<pkg_id>#<author_did>#<name>[,…] (ext.list_library names them)', required: true }],
|
|
682
|
+
options: [WORKSPACE_OPTION, { name: 'folder', property: 'folder_id', description: 'Data folder for the definitions (default: data/extensions)', type: 'string' }],
|
|
683
|
+
inputSchema: workspaceInput({ items: { type: 'string', minLength: 1 }, folder_id: { type: 'string' } }, ['items']),
|
|
684
|
+
outputSchema: OBJECT_OUTPUT,
|
|
685
|
+
resultSchemaVersion: 1,
|
|
686
|
+
access: { mode: 'fixed', level: 'write' },
|
|
687
|
+
asyncMode: 'sync',
|
|
688
|
+
requiresSession: true,
|
|
689
|
+
examples: ['buckyos aiws ext-import kpi-pack.alice.bns.did#did:bns:alice#kpi-card --workspace ws_abc'],
|
|
690
|
+
handler: (ctx, input) => {
|
|
691
|
+
const items = list(input, 'items').map((item) => {
|
|
692
|
+
const cut = item.indexOf('#')
|
|
693
|
+
if (cut <= 0) throw new UsageError('AIWS_USAGE', `${item}: <pkg_id>#<ident>`)
|
|
694
|
+
return { pkg_id: item.slice(0, cut), ident: item.slice(cut + 1) }
|
|
695
|
+
})
|
|
696
|
+
const folder = inputString(input, 'folder_id')
|
|
697
|
+
return aiws(ctx, 'ext.import', { workspace_id: requiredInputString(input, 'workspace_id'), items, ...(folder ? { folder_id: folder } : {}) })
|
|
698
|
+
},
|
|
699
|
+
}
|
|
700
|
+
|
|
378
701
|
export function createAiwsModule(): CommandModule {
|
|
379
702
|
return {
|
|
380
703
|
name: 'aiws',
|
|
381
|
-
summary: 'AI Workspace: read a workspace, Block-controls (catalogue, import, export), and execute wish runs as their executor (claim, heartbeat, report)',
|
|
382
|
-
commands: [infoCommand, readCommand, queryCommand, controlsCommand, importCommand, exportCommand, runGetCommand, runClaimCommand, runHeartbeatCommand, runFilesCommand, runToolCommand, runProgramCommand, runReportCommand],
|
|
704
|
+
summary: 'AI Workspace: read a workspace, Block-controls (catalogue, import, export), extension controls (build, pull, export, library, import), and execute wish runs as their executor (claim, heartbeat, report)',
|
|
705
|
+
commands: [infoCommand, readCommand, queryCommand, controlsCommand, importCommand, exportCommand, buildCommand, pullCommand, extExportCommand, libraryCommand, extImportCommand, runGetCommand, runClaimCommand, runHeartbeatCommand, runFilesCommand, runToolCommand, runProgramCommand, runReportCommand],
|
|
383
706
|
}
|
|
384
707
|
}
|
package/cli/modules/pikg.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommandContext } from '../core/context.ts'
|
|
2
2
|
import type { CommandModule } from '../core/command.ts'
|
|
3
3
|
import { EXIT_PERMISSION, EXIT_UNAVAILABLE, ToolError, UsageError } from '../core/errors.ts'
|
|
4
|
+
import { AIWS_MANIFEST_FILE, AIWS_MANIFEST_VERSION, aiwsContentDigest, controlSkeleton } from 'buckyos/node'
|
|
4
5
|
import { createDeterministicTarGz, digestFile, sha256Bytes } from './pikg_archive.ts'
|
|
5
6
|
import {
|
|
6
7
|
APPDOC_ENTRY,
|
|
@@ -55,11 +56,13 @@ const SHA256_ID = /^sha256:[0-9a-f]{64}$/
|
|
|
55
56
|
const OBJECT_ID = /^(?:appdoc|pkg):[0-9a-f]{64}$/
|
|
56
57
|
const SAFE_GENERATED_FILE = /^[A-Za-z0-9._-]+$/
|
|
57
58
|
const OBJECT_OUTPUT = { type: 'object' as const, additionalProperties: true }
|
|
58
|
-
const APP_KINDS = ['static-web', 'script', 'docker', 'agent-skills']
|
|
59
|
+
const APP_KINDS = ['static-web', 'script', 'docker', 'agent-skills', 'aiws-controls']
|
|
59
60
|
const PATH_KINDS: Record<string, { key: string; category: string; selectorType: string }> = {
|
|
60
61
|
'static-web': { key: 'web', category: 'web', selectorType: 'static' },
|
|
61
62
|
script: { key: 'script', category: 'dapp', selectorType: 'single' },
|
|
62
63
|
'agent-skills': { key: 'agent_skills', category: 'agent_skills', selectorType: 'single' },
|
|
64
|
+
// AI Workspace controls: one bundle (aiws-manifest.json + controls/<dir>/), installed into the AI Workspace library
|
|
65
|
+
'aiws-controls': { key: 'aiws', category: 'aiws', selectorType: 'single' },
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export interface DockerImageInfo {
|
|
@@ -243,6 +246,7 @@ export function createPikgModule(dependencies: PikgModuleDependencies = {}): Com
|
|
|
243
246
|
'buckyos pikg init .',
|
|
244
247
|
'buckyos --non-interactive pikg init . --owner did:bns:root --kind static-web --source ./web/dist',
|
|
245
248
|
'buckyos --non-interactive pikg init . --owner did:bns:root --kind agent-skills --source ./skills',
|
|
249
|
+
'buckyos --non-interactive pikg init . --owner did:bns:alice --kind aiws-controls --source ./aiws',
|
|
246
250
|
],
|
|
247
251
|
handler: async (ctx, input) => await initCommand(ctx, input, docker),
|
|
248
252
|
},
|
|
@@ -493,9 +497,13 @@ async function initCommand(
|
|
|
493
497
|
try {
|
|
494
498
|
await getHost().lstat(absoluteSource)
|
|
495
499
|
} catch (error) {
|
|
496
|
-
if (isHostError(error, 'NotFound'))
|
|
500
|
+
if (!isHostError(error, 'NotFound')) throw error
|
|
501
|
+
if (kind === 'aiws-controls') {
|
|
502
|
+
await writeAiwsSkeleton(absoluteSource, owner, name)
|
|
503
|
+
sourceSummary.skeleton = true
|
|
504
|
+
} else {
|
|
497
505
|
await ctx.io.stderr(`warning: source does not exist yet: ${sourceSummary.path}\n`)
|
|
498
|
-
}
|
|
506
|
+
}
|
|
499
507
|
}
|
|
500
508
|
}
|
|
501
509
|
|
|
@@ -557,6 +565,24 @@ async function initCommand(
|
|
|
557
565
|
}
|
|
558
566
|
}
|
|
559
567
|
|
|
568
|
+
/** A control package skeleton: one declarative control under controls/<name>/ and its manifest (digest included). */
|
|
569
|
+
async function writeAiwsSkeleton(dir: string, ownerDid: string, name: string): Promise<void> {
|
|
570
|
+
const files = controlSkeleton({ authorDid: ownerDid, name, title: name })
|
|
571
|
+
const encoder = new TextEncoder()
|
|
572
|
+
const host = getHost()
|
|
573
|
+
await host.mkdir(join(dir, 'controls', name), { recursive: true })
|
|
574
|
+
for (const [file, text] of Object.entries(files)) await host.writeTextFile(join(dir, 'controls', name, file), text)
|
|
575
|
+
const digest = await aiwsContentDigest(Object.entries(files).map(([file, text]) => [file, encoder.encode(text)] as [string, Uint8Array]))
|
|
576
|
+
const manifest = {
|
|
577
|
+
manifest_version: AIWS_MANIFEST_VERSION,
|
|
578
|
+
purpose: 'aiws',
|
|
579
|
+
host_api: { declarative: 1 },
|
|
580
|
+
controls: [{ ident: `${ownerDid}#${name}`, name, kind: 'declarative', version: 1, digest, dir: `controls/${name}/`, title: name }],
|
|
581
|
+
workspace_templates: [],
|
|
582
|
+
}
|
|
583
|
+
await host.writeTextFile(join(dir, AIWS_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
584
|
+
}
|
|
585
|
+
|
|
560
586
|
async function buildCommand(
|
|
561
587
|
ctx: CommandContext,
|
|
562
588
|
input: Record<string, unknown>,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { ndn } from 'buckyos/node'
|
|
1
|
+
import { ndn, verifyBundle } from 'buckyos/node'
|
|
2
2
|
import { ToolError, UsageError } from '../core/errors.ts'
|
|
3
3
|
import { getHost } from '../runtime/host.ts'
|
|
4
4
|
import {
|
|
5
5
|
digestFile,
|
|
6
6
|
type FileDigest,
|
|
7
7
|
openZip,
|
|
8
|
+
readTarGz,
|
|
8
9
|
readZipEntry,
|
|
9
10
|
sha256Bytes,
|
|
10
11
|
verifyZipEntry,
|
|
@@ -69,6 +70,8 @@ export interface ValidatedPikg {
|
|
|
69
70
|
|
|
70
71
|
export interface PikgInspectionResult {
|
|
71
72
|
schema_version: 1
|
|
73
|
+
/** An `aiws` package: its bundle checked like the AI Workspace library does. */
|
|
74
|
+
aiws?: Record<string, unknown>
|
|
72
75
|
protocol: string
|
|
73
76
|
pikg_path: string
|
|
74
77
|
size: number
|
|
@@ -141,6 +144,7 @@ export function derivedSelector(key: string): Record<string, string> | undefined
|
|
|
141
144
|
agent: {},
|
|
142
145
|
agent_skills: {},
|
|
143
146
|
agent_tools: {},
|
|
147
|
+
aiws: {},
|
|
144
148
|
script: {},
|
|
145
149
|
}
|
|
146
150
|
return selectors[key]
|
|
@@ -505,6 +509,27 @@ export function appDocObjectId(value: Record<string, unknown>): string {
|
|
|
505
509
|
return ndn.buildNamedObjectByJson('appdoc', value)[0].toString()
|
|
506
510
|
}
|
|
507
511
|
|
|
512
|
+
/** The sub-package of an `aiws` package: the AIWS bundle (aiws-manifest.json plus controls/<dir>/<file>). */
|
|
513
|
+
export const AIWS_SUB_PACKAGE = 'aiws'
|
|
514
|
+
const MAX_AIWS_PAYLOAD_BYTES = 256 * 1024 * 1024
|
|
515
|
+
|
|
516
|
+
/** Check the bundle inside an `aiws` payload the way the AI Workspace library does (digests, component.json). */
|
|
517
|
+
export async function verifyAiwsPayload(tarGz: Uint8Array): Promise<Record<string, unknown>> {
|
|
518
|
+
const files = new Map<string, Uint8Array>()
|
|
519
|
+
for (const entry of await readTarGz(tarGz, MAX_AIWS_PAYLOAD_BYTES)) files.set(entry.path, entry.bytes)
|
|
520
|
+
let checked
|
|
521
|
+
try {
|
|
522
|
+
checked = await verifyBundle(files)
|
|
523
|
+
} catch (error) {
|
|
524
|
+
throw invalid('aiws', `aiws bundle: ${error instanceof Error ? error.message : String(error)}`)
|
|
525
|
+
}
|
|
526
|
+
const errors = checked.diagnostics.filter((d: { severity: string }) => d.severity === 'error')
|
|
527
|
+
if (errors.length > 0) {
|
|
528
|
+
throw invalid('aiws', `aiws bundle: ${errors.map((d: { file: string; message: string }) => `${d.file}: ${d.message}`).join('; ')}`)
|
|
529
|
+
}
|
|
530
|
+
return { manifest_version: checked.manifest.manifest_version, controls: checked.manifest.controls.map((c: Record<string, unknown>) => ({ ident: c.ident, kind: c.kind, version: c.version, digest: c.digest })) }
|
|
531
|
+
}
|
|
532
|
+
|
|
508
533
|
export async function validateSnapshot(
|
|
509
534
|
distDir: string,
|
|
510
535
|
appDoc: Record<string, unknown>,
|
|
@@ -517,7 +542,11 @@ export async function validateSnapshot(
|
|
|
517
542
|
const digest = await digestFile(path)
|
|
518
543
|
payloads.set(entry.path, digest)
|
|
519
544
|
}
|
|
520
|
-
|
|
545
|
+
const validated = validateObjectGraph(appDoc, packageMeta, payloads, true, false, true)
|
|
546
|
+
if (appDoc.app_type === 'aiws') {
|
|
547
|
+
for (const sub of validated.subpackages) await verifyAiwsPayload(await getHost().readFile(join(distDir, sub.payload_path)))
|
|
548
|
+
}
|
|
549
|
+
return validated
|
|
521
550
|
}
|
|
522
551
|
|
|
523
552
|
export async function packSnapshot(
|
|
@@ -636,9 +665,15 @@ export async function inspectPikg(path: string): Promise<PikgInspectionResult> {
|
|
|
636
665
|
Boolean(jwtEntry),
|
|
637
666
|
!jsonDoc || !jwtDoc || appDocObjectId(jsonDoc) === appDocObjectId(jwtDoc),
|
|
638
667
|
)
|
|
668
|
+
let aiws: Record<string, unknown> | undefined
|
|
669
|
+
if (appDoc.app_type === 'aiws') {
|
|
670
|
+
const sub = validated.subpackages[0]
|
|
671
|
+
aiws = await verifyAiwsPayload(await readZipEntry(archive, archive.byName.get(sub.payload_path)!, MAX_AIWS_PAYLOAD_BYTES))
|
|
672
|
+
}
|
|
639
673
|
const pikgDigest = await digestFile(path)
|
|
640
674
|
return {
|
|
641
675
|
schema_version: 1,
|
|
676
|
+
...(aiws ? { aiws } : {}),
|
|
642
677
|
protocol: PACKAGE_META_SCHEMA,
|
|
643
678
|
pikg_path: resolve(path),
|
|
644
679
|
size: pikgDigest.size,
|
|
@@ -954,7 +989,7 @@ export function validateAppDocShape(appDoc: Record<string, unknown>): void {
|
|
|
954
989
|
expectNonEmptyString(appDoc[field], `APPDOC.${field}`)
|
|
955
990
|
}
|
|
956
991
|
if (appDoc.doc_type !== 'app') throw invalid('appdoc', 'APPDOC.doc_type must be app')
|
|
957
|
-
if (!['service', 'dapp', 'web', 'agent', 'agent_skills'].includes(String(appDoc.app_type))) {
|
|
992
|
+
if (!['service', 'dapp', 'web', 'agent', 'agent_skills', 'aiws'].includes(String(appDoc.app_type))) {
|
|
958
993
|
throw invalid('appdoc', 'APPDOC.app_type is invalid')
|
|
959
994
|
}
|
|
960
995
|
if (appDoc.name !== undefined) expectNonEmptyString(appDoc.name, 'APPDOC.name')
|
|
@@ -1014,6 +1049,10 @@ export function validateAppDocShape(appDoc: Record<string, unknown>): void {
|
|
|
1014
1049
|
throw invalid('appdoc', 'an agent_skills App must contain only the agent_skills sub package')
|
|
1015
1050
|
}
|
|
1016
1051
|
}
|
|
1052
|
+
// AI Workspace controls (buckyos aiworkspace README “Extension controls”): one bundle, nothing to run
|
|
1053
|
+
if (appDoc.app_type === 'aiws' && Object.keys(pkgList).join() !== AIWS_SUB_PACKAGE) {
|
|
1054
|
+
throw invalid('appdoc', 'an aiws package must contain only the aiws sub package')
|
|
1055
|
+
}
|
|
1017
1056
|
deriveNamespaceForPackage(appDoc)
|
|
1018
1057
|
}
|
|
1019
1058
|
|
package/cli/modules/share.ts
CHANGED
|
@@ -65,7 +65,8 @@ async function openNfsp(ctx: CommandContext): Promise<NfspSession> {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
/** The Zone gateway origin of a service endpoint (`…/kapi/<service>`): where `/nfs/v1` and other routes live. */
|
|
69
|
+
export function gatewayOrigin(endpoint: string): string {
|
|
69
70
|
const url = new URL(endpoint)
|
|
70
71
|
const marker = url.pathname.indexOf('/kapi')
|
|
71
72
|
url.pathname = marker >= 0 ? url.pathname.slice(0, marker) || '/' : '/'
|
package/cli/version.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-cli-version.mjs. Do not edit.
|
|
2
|
-
export const PACKAGE_VERSION = '0.7.
|
|
2
|
+
export const PACKAGE_VERSION = '0.7.160'
|
|
3
3
|
export const TOOL_VERSION = PACKAGE_VERSION
|
|
4
4
|
export const SDK_VERSION = PACKAGE_VERSION
|
|
5
5
|
export const PROTOCOL_VERSION = '1'
|