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.
@@ -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
- throw new ToolError(code, `${code}${typeof error.sub_code === 'string' ? `/${error.sub_code}` : ''}: ${detail}`)
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
  }
@@ -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
- } else throw error
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
- return validateObjectGraph(appDoc, packageMeta, payloads, true, false, true)
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
 
@@ -65,7 +65,8 @@ async function openNfsp(ctx: CommandContext): Promise<NfspSession> {
65
65
  }
66
66
  }
67
67
 
68
- function gatewayOrigin(endpoint: string): string {
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.159'
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'