@frontera-sdk/cli 1.52.38 → 1.52.40
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/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +48 -4
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +331 -31
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +875 -88
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +320 -13
- package/src/commands/local/sync.ts +102 -5
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +52 -8
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +112 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +211 -21
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +180 -7
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +346 -35
- package/src/local/local-dev-sync.ts +136 -7
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +205 -7
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +141 -16
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +38 -8
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +1 -1
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +11 -0
- package/src/local/local-sync-push.ts +10 -7
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +343 -32
- package/src/local/local-sync-snapshot.ts +15 -10
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +24 -21
|
@@ -3,8 +3,9 @@ import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync
|
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join, posix, relative } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
|
|
6
|
+
import { Document as YamlDocument, isSeq, parse as parseYaml, stringify as stringifyYaml, type YAMLMap, type YAMLSeq } from 'yaml'
|
|
7
7
|
|
|
8
|
+
import { semanticFactsOf } from '../blueprint/action-semantic-fit'
|
|
8
9
|
import { toFiles } from '../blueprint/projection'
|
|
9
10
|
import { serialize } from '../blueprint/tree'
|
|
10
11
|
import { assetFilename, toSkillMd, type SkillDoc } from '../commands/skill/bundle-commands'
|
|
@@ -20,11 +21,15 @@ import { readChatApps, type ChatAppRow } from './local-chat-apps'
|
|
|
20
21
|
import { readApps, type AppRow as CustomAppRow } from './local-apps'
|
|
21
22
|
import { readBrand, type BrandRow } from './local-brand'
|
|
22
23
|
import { readLibrary, type LibraryRow } from './local-library'
|
|
24
|
+
import { fromOutsidePath, outsidePath } from '../function-outside-imports'
|
|
23
25
|
import { isSharedFunctionFile } from './local-function-shared'
|
|
26
|
+
import { hardCodedObjectTypeIds, platformObjectTypeIds } from './local-function-ids'
|
|
27
|
+
import { missingImportNote, missingImportWarning, recoverMissingImports, type MissingImports } from './local-function-recovery'
|
|
24
28
|
import { attachDefinitions, type ObjectDefinitionRow } from './local-objects'
|
|
25
29
|
import { checksFromFixtures, fixtureRows, qualityChecksPath, qualityChecksText, sourcePaths, sourceRows } from './local-quality-checks'
|
|
26
30
|
import { readableName } from './local-sync-names'
|
|
27
|
-
import {
|
|
31
|
+
import { WORKSPACE_GRANTS_FILE, WORKSPACE_GRANTS_NAME } from './local-workspace-grants'
|
|
32
|
+
import { governedActionIds, invert, itemRef, referencesIn, refName as refNameOf, toNames, type Directory, type RefKind } from './local-sync-refs'
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* Reading a workspace as the files a project holds. Each kind lists what the
|
|
@@ -57,9 +62,9 @@ function one<T>(body: unknown): T {
|
|
|
57
62
|
return v as T
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
/** A resource's name as a reference can hold it: knowledge bases are named in words. */
|
|
65
|
+
/** A resource's name as a reference can hold it: knowledge bases are named in words (`local-sync-refs.ts`). */
|
|
61
66
|
export function refName(name: string): string {
|
|
62
|
-
return
|
|
67
|
+
return refNameOf(name)
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
async function quietly<T>(warnings: string[], what: string, read: () => Promise<T>, empty: T): Promise<T> {
|
|
@@ -382,9 +387,11 @@ export interface AgentBindings {
|
|
|
382
387
|
/**
|
|
383
388
|
* Its Context and data: the object types it may read through its Blueprint
|
|
384
389
|
* tools (`objectType/Incident`), with the permissions granted. Absent when
|
|
385
|
-
* it has none, or they could not be read (no organization key).
|
|
390
|
+
* it has none, or they could not be read (no organization key). An entry
|
|
391
|
+
* without `permissions` was read by name only: `local dev` and a change
|
|
392
|
+
* request then keep what the grant holds (`act`, `read_sensitive`).
|
|
386
393
|
*/
|
|
387
|
-
blueprint?: Array<{ objectType: string; permissions
|
|
394
|
+
blueprint?: Array<{ objectType: string; permissions?: string[] }>
|
|
388
395
|
/**
|
|
389
396
|
* Its Context and data could not be read on the platform with the project's
|
|
390
397
|
* keys. `local dev` then gives it read access to the object types the
|
|
@@ -439,8 +446,10 @@ export async function readAgentContext(source: SyncSource, row: AgentRow): Promi
|
|
|
439
446
|
// An empty answer may only mean the key can't see the Agent there, so it is not taken for "none".
|
|
440
447
|
if (listed.length) {
|
|
441
448
|
return {
|
|
442
|
-
|
|
443
|
-
|
|
449
|
+
// No `permissions`: the file doesn't state what it didn't read, so applying it keeps what each grant
|
|
450
|
+
// holds. Written as `["read"]`, a change request or local dev would take a held `act` away.
|
|
451
|
+
grants: listed.map((n) => ({ objectType: `objectType/${n}` })).sort((a, b) => a.objectType.localeCompare(b.objectType)),
|
|
452
|
+
note: 'Its Context and data was read as the object types it may use: this platform doesn’t give their permissions to this key, so each is written without permissions, and applying the file keeps what each grant holds (read, and act or read_sensitive where it has them)',
|
|
444
453
|
}
|
|
445
454
|
}
|
|
446
455
|
return { unread: refused ?? `this platform gives it only to an organization key. ${STEWARD_FIX}` }
|
|
@@ -473,6 +482,81 @@ async function readBindings(source: SyncSource, lists: Lists, warnings: string[]
|
|
|
473
482
|
return out
|
|
474
483
|
}
|
|
475
484
|
|
|
485
|
+
const NO_SETTINGS = ': it runs on the defaults there, and the same defaults here; `frontera local schema operating` says what the file can set'
|
|
486
|
+
|
|
487
|
+
/** Settings an Agent has: an object with at least one key. */
|
|
488
|
+
function settingsIn(value: unknown): Record<string, unknown> | null {
|
|
489
|
+
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length ? (value as Record<string, unknown>) : null
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* An Agent's behavior settings from their own route (`/v1/config/agents/:id/operating`):
|
|
494
|
+
* the live ones, or the draft's when the draft is what was read. The last
|
|
495
|
+
* place a pull looks, after the snapshot and the Agent's own record (both of
|
|
496
|
+
* which a released platform gives a workspace key, scrubbed). A note in every
|
|
497
|
+
* case: where they came from, that the Agent has none (so it is pulled
|
|
498
|
+
* without operating.json, and `local dev` uses the runtime's defaults), or why
|
|
499
|
+
* they could not be read. A released platform keeps the route to a signed-in
|
|
500
|
+
* person, so its refusal (or a platform without it) is taken for what the
|
|
501
|
+
* snapshot said: none.
|
|
502
|
+
*/
|
|
503
|
+
export async function readOperatingConfig(source: SyncSource, agentId: string, draft: boolean): Promise<{ config: Record<string, unknown>; note: string } | { note: string }> {
|
|
504
|
+
try {
|
|
505
|
+
const view = one<{ configured?: boolean; live?: unknown; draft?: unknown }>(await source.workspace.get(`/v1/config/agents/${encodeURIComponent(agentId)}/operating`))
|
|
506
|
+
const chosen = settingsIn(draft ? view?.draft ?? view?.live : view?.live)
|
|
507
|
+
if (chosen) return { config: chosen, note: `Its behavior settings were read from the platform${draft && view?.draft ? '’s draft' : ''} into operating.json` }
|
|
508
|
+
return { note: `Has no behavior settings in the workspace (operating.json)${NO_SETTINGS}` }
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const code = err instanceof CliError ? err.code : null
|
|
511
|
+
if (code === 'UNAUTHORIZED' || code === 'FORBIDDEN' || code === 'NOT_FOUND') return { note: `Has no behavior settings in its snapshot on the platform (operating.json)${NO_SETTINGS}` }
|
|
512
|
+
return { note: `Its behavior settings couldn’t be read on the platform (${(err as Error).message.slice(0, 160)}), so no operating.json was pulled: if it has some there, they aren’t here` }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* What a pull says of behavior settings without `execution.toolScopes` (as a
|
|
518
|
+
* released platform gives them): they keep the meaning tool ids had before
|
|
519
|
+
* Blueprint Actions were their own choice, here as there, and `local dev`
|
|
520
|
+
* warns about it. Null when the marker is there.
|
|
521
|
+
*/
|
|
522
|
+
export function legacyToolScopesNote(config: Record<string, unknown>): string | null {
|
|
523
|
+
const execution = settingsIn(config.execution) ?? {}
|
|
524
|
+
if (execution.toolScopes === 2) return null
|
|
525
|
+
return 'Its behavior settings have no `execution.toolScopes`, as the workspace keeps them, so they keep the meaning tool ids had before Blueprint Actions were their own choice (`tool:blueprint` keeps the Action tools, and a service Agent has them outside procedures), here as there; `frontera local dev` warns about it until "toolScopes": 2 is added to "execution"'
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* An Agent's behavior settings, from the first place that has them:
|
|
530
|
+
* 1. the snapshot that was read (`draft-v2` for a draft, else `snapshot/live`), its `config.operatingConfig`;
|
|
531
|
+
* 2. for a live read, the Agent's record (`/v1/config/agents/:id`), then the listing's row;
|
|
532
|
+
* 3. for a draft read with none, none: the draft is the whole of them;
|
|
533
|
+
* 4. only then their own route (`readOperatingConfig`).
|
|
534
|
+
*/
|
|
535
|
+
export async function agentOperatingConfig(
|
|
536
|
+
source: SyncSource,
|
|
537
|
+
agent: { id: string; operatingConfig?: unknown },
|
|
538
|
+
read: { snapshot: Record<string, unknown>; fromDraft: boolean; detail?: { operatingConfig?: unknown } | null },
|
|
539
|
+
): Promise<{ config: Record<string, unknown> | null; notes: string[] }> {
|
|
540
|
+
const fromSnapshot = settingsIn((read.snapshot.config as Record<string, unknown> | undefined)?.operatingConfig)
|
|
541
|
+
const found = fromSnapshot ?? (read.fromDraft ? null : settingsIn(read.detail?.operatingConfig) ?? settingsIn(agent.operatingConfig))
|
|
542
|
+
const notes: string[] = []
|
|
543
|
+
let config = found
|
|
544
|
+
if (!config) {
|
|
545
|
+
if (read.fromDraft) notes.push(`Has no behavior settings in its draft (operating.json)${NO_SETTINGS}`)
|
|
546
|
+
else {
|
|
547
|
+
const route = await readOperatingConfig(source, agent.id, false)
|
|
548
|
+
if ('config' in route) config = route.config
|
|
549
|
+
notes.push(route.note)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const legacy = config ? legacyToolScopesNote(config) : null
|
|
553
|
+
if (legacy) notes.push(legacy)
|
|
554
|
+
return { config, notes }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** An Agent's record (`/v1/config/agents/:id`): its published version, and its behavior settings, scrubbed. */
|
|
558
|
+
interface AgentDetail { currentVersion?: { versionNumber?: number } | null; operatingConfig?: unknown }
|
|
559
|
+
|
|
476
560
|
async function readAgents(source: SyncSource, lists: Lists, directory: Directory, opts: ReadOptions, warnings: string[], tools: () => Promise<PluginTools>): Promise<RemoteItem[]> {
|
|
477
561
|
const out: RemoteItem[] = []
|
|
478
562
|
const agents = lists.agents.filter((row) => wanted(opts, `agent/${row.agentId}`))
|
|
@@ -483,30 +567,36 @@ async function readAgents(source: SyncSource, lists: Lists, directory: Directory
|
|
|
483
567
|
const wantDraft = opts.drafts?.has(ref) || !row.currentVersionId
|
|
484
568
|
const it = item('agent', row.agentId, row.name, {}, { id: row.id })
|
|
485
569
|
let snapshot: Record<string, unknown> | null = null
|
|
570
|
+
let detail: AgentDetail | null = null
|
|
571
|
+
let fromDraft = false
|
|
486
572
|
try {
|
|
487
573
|
if (wantDraft) {
|
|
488
574
|
const draft = one<{ snapshot?: Record<string, unknown>; revision?: number } | null>(await source.workspace.get(`/v1/config/agents/${row.id}/draft-v2`))
|
|
489
575
|
snapshot = draft?.snapshot ?? null
|
|
576
|
+
fromDraft = !!snapshot
|
|
490
577
|
if (!row.currentVersionId) it.notes.push('Not published on the platform: its draft was read')
|
|
491
578
|
else it.notes.push('Read from its draft, as asked')
|
|
492
579
|
if (draft?.revision !== undefined) it.version = `draft ${draft.revision}`
|
|
493
580
|
}
|
|
494
581
|
if (!snapshot) {
|
|
495
582
|
snapshot = one<Record<string, unknown>>(await source.workspace.get(`/v1/config/agents/${row.id}/snapshot/live`))
|
|
496
|
-
|
|
583
|
+
detail = one<AgentDetail | null>(await source.workspace.get(`/v1/config/agents/${row.id}`))
|
|
497
584
|
if (detail?.currentVersion?.versionNumber) it.version = `v${detail.currentVersion.versionNumber}`
|
|
498
585
|
}
|
|
499
586
|
} catch (err) {
|
|
500
587
|
warnings.push(`Agent ${row.agentId}: ${(err as Error).message}`)
|
|
501
588
|
continue
|
|
502
589
|
}
|
|
503
|
-
|
|
590
|
+
// Where its behavior settings are (the snapshot, the Agent's record, their own route), or that it has none, said.
|
|
591
|
+
const behavior = await agentOperatingConfig(source, row, { snapshot, fromDraft, detail })
|
|
592
|
+
const operating = behavior.config
|
|
593
|
+
it.notes.push(...behavior.notes)
|
|
504
594
|
const toolSettings = Array.isArray(snapshot.appCapabilities) && snapshot.appCapabilities.length ? { apps: lists.apps, tools: await tools() } : undefined
|
|
505
595
|
const agentDoc = projectAgent(snapshot, it.notes, toolSettings)
|
|
506
596
|
// Its object tools carry the definitions they work on, named by slug below.
|
|
507
597
|
it.notes.push(...attachDefinitions(agentDoc, lists.objectDefinitions ?? null))
|
|
508
598
|
const docs: Array<{ path: string; doc: unknown; format: 'json' }> = [{ path: `agents/${row.agentId}/agent.json`, doc: agentDoc, format: 'json' }]
|
|
509
|
-
if (operating
|
|
599
|
+
if (operating) docs.push({ path: `agents/${row.agentId}/operating.json`, doc: operating, format: 'json' })
|
|
510
600
|
const bound: AgentBindings = { ...(bindings.get(row.agentId) ?? { functions: [], workflows: [] }) }
|
|
511
601
|
// Context and data comes as the Agent's dependency: each object type it names is pulled with it.
|
|
512
602
|
const context = await readAgentContext(source, row)
|
|
@@ -583,15 +673,129 @@ function unpack(bytes: Uint8Array): Map<string, string> {
|
|
|
583
673
|
}
|
|
584
674
|
}
|
|
585
675
|
|
|
586
|
-
/** The
|
|
587
|
-
|
|
676
|
+
/** The project files a Function's folder keeps beside its code. */
|
|
677
|
+
const FOLDER_PROJECT_FILES = ['package.json', 'tsconfig.json', 'bunfig.toml', 'README.md']
|
|
678
|
+
|
|
679
|
+
/** The SDK's own copy inside a Function project (`function init` vendors it there); the solution keeps one in vendor/. */
|
|
680
|
+
const FOLDER_SDK = /^(src\/)?frontera\//
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* The entry in a folder Function's source archive: the file whose
|
|
684
|
+
* `automation({ name })` is this Function (one archive can hold every
|
|
685
|
+
* Function of a project), else `<slug>.ts`, `src/<slug>.ts`, `index.ts`,
|
|
686
|
+
* `src/index.ts`; the rule `functionEntry` applies to a project folder.
|
|
687
|
+
*/
|
|
688
|
+
export function archiveEntry(files: ReadonlyMap<string, string>, slug: string): string | null {
|
|
689
|
+
const code = [...files.keys()].filter((p) => p.endsWith('.ts') && !p.endsWith('.test.ts') && !p.endsWith('.d.ts') && !FOLDER_SDK.test(p)).sort()
|
|
690
|
+
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
691
|
+
const declares = new RegExp(`['"]?name['"]?\\s*:\\s*['"\`]${escaped}['"\`]`)
|
|
692
|
+
const named = code.find((p) => /automation\s*\(/.test(files.get(p)!) && declares.test(files.get(p)!))
|
|
693
|
+
return named ?? [`${slug}.ts`, `src/${slug}.ts`, 'index.ts', 'src/index.ts'].find((p) => files.has(p)) ?? null
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* What a folder Function's pull writes, relative to `functions/<slug>/`: its
|
|
698
|
+
* entry and the files it imports, its test, and the folder's project files,
|
|
699
|
+
* with the tsconfig's SDK paths pointed at the solution's `vendor/frontera/`.
|
|
700
|
+
* Without an entry, every file but the SDK copy (and `entry` is null).
|
|
701
|
+
*/
|
|
702
|
+
export function folderFunctionFiles(
|
|
703
|
+
files: ReadonlyMap<string, string>,
|
|
704
|
+
slug: string,
|
|
705
|
+
): { entry: string | null; files: Record<string, string>; outside: Record<string, string>; missing: string[] } {
|
|
706
|
+
const entry = archiveEntry(files, slug)
|
|
707
|
+
const out: Record<string, string> = {}
|
|
708
|
+
// What it imports from outside its folder, by its path from the folder (`../shared/x.ts`): kept in
|
|
709
|
+
// the archive under `frontera-imports/` by a current deploy; `missing` when the archive lacks it.
|
|
710
|
+
const outside: Record<string, string> = {}
|
|
711
|
+
const missing: string[] = []
|
|
712
|
+
if (!entry) {
|
|
713
|
+
for (const [p, content] of files) if (!p.startsWith('.') && !FOLDER_SDK.test(p) && !fromOutsidePath(p)) out[p] = content
|
|
714
|
+
} else {
|
|
715
|
+
const code = withImports(files as Map<string, string>, entry, missing).filter((p) => !FOLDER_SDK.test(p))
|
|
716
|
+
const test = entry.replace(/\.ts$/, '.test.ts')
|
|
717
|
+
for (const p of [...code, ...(files.has(test) ? [test] : []), ...FOLDER_PROJECT_FILES.filter((f) => files.has(f))]) {
|
|
718
|
+
const beyond = fromOutsidePath(p)
|
|
719
|
+
if (beyond) outside[beyond] = files.get(p)!
|
|
720
|
+
else out[p] = files.get(p)!
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (out['tsconfig.json']) out['tsconfig.json'] = vendorPaths(out['tsconfig.json'])
|
|
724
|
+
Object.assign(out, sdkShims(out))
|
|
725
|
+
return { entry, files: out, outside, missing: [...new Set(missing)].sort() }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** The first line of a file a pull writes to point a folder's own SDK path at the solution's copy. */
|
|
729
|
+
export const SDK_SHIM_HEADER = '// Written by `frontera local sync pull`: this Function imports its folder’s own copy of the SDK, which the project keeps once, in vendor/frontera/.'
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Files that point a Function folder's own SDK copy (`./frontera/automation/testing`,
|
|
733
|
+
* which `function init` vendored beside its code) at the solution's `vendor/frontera/`.
|
|
734
|
+
* A pull leaves the folder's copy out (the project keeps one), and a relative
|
|
735
|
+
* import cannot be mapped by tsconfig `paths`; so each such import gets a file
|
|
736
|
+
* that re-exports the solution's module, and the pulled code stays as it was.
|
|
737
|
+
* Keyed by path in the folder (`src/frontera/automation/testing.ts`).
|
|
738
|
+
*/
|
|
739
|
+
export function sdkShims(files: Readonly<Record<string, string>>): Record<string, string> {
|
|
740
|
+
const shims: Record<string, string> = {}
|
|
741
|
+
for (const [path, text] of Object.entries(files)) {
|
|
742
|
+
if (!/\.(ts|tsx|mts)$/.test(path)) continue
|
|
743
|
+
for (const m of text.matchAll(/(?:from|import)\s*\(?\s*['"](\.{1,2}\/[^'"]+)['"]/g)) {
|
|
744
|
+
const target = posix.normalize(posix.join(posix.dirname(path), m[1]!)).replace(/\.(ts|js)$/, '')
|
|
745
|
+
if (!FOLDER_SDK.test(target) || target.startsWith('../')) continue
|
|
746
|
+
const shim = `${target}.ts`
|
|
747
|
+
if (files[shim] || files[`${target}/index.ts`] || shims[shim]) continue
|
|
748
|
+
// From functions/<slug>/<shim's folder> up to the project root, then into vendor/.
|
|
749
|
+
const up = posix.relative(posix.join('functions', '_', posix.dirname(shim)), '.')
|
|
750
|
+
const module = target.replace(/^(src\/)?/, '')
|
|
751
|
+
shims[shim] = `${SDK_SHIM_HEADER}\nexport * from '${up}/vendor/${module}'\n`
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return shims
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** A Function folder's tsconfig with its `@frontera-sdk/*` paths at the solution's vendor/frontera/ (two folders up). */
|
|
758
|
+
export function vendorPaths(tsconfig: string): string {
|
|
759
|
+
let doc: { compilerOptions?: { baseUrl?: string; paths?: Record<string, string[]> } }
|
|
760
|
+
try {
|
|
761
|
+
doc = JSON.parse(tsconfig)
|
|
762
|
+
} catch {
|
|
763
|
+
return tsconfig
|
|
764
|
+
}
|
|
765
|
+
const paths = doc.compilerOptions?.paths
|
|
766
|
+
if (!paths) return tsconfig
|
|
767
|
+
const base = doc.compilerOptions!.baseUrl ?? '.'
|
|
768
|
+
const up = posix.relative(posix.normalize(base), '../..') || '.'
|
|
769
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
770
|
+
if (!alias.startsWith('@frontera-sdk/')) continue
|
|
771
|
+
paths[alias] = targets.map((t) => t.replace(/^(\.\/)?(src\/)?frontera\//, `${up}/vendor/frontera/`))
|
|
772
|
+
}
|
|
773
|
+
return `${JSON.stringify(doc, null, 2)}\n`
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* The file and the relative files it imports, transitively, within the
|
|
778
|
+
* archive. An import that leaves the archive's folder (`../shared/x`) is
|
|
779
|
+
* found under `frontera-imports/` (`outsidePath`), where a current deploy
|
|
780
|
+
* keeps it; one the archive lacks is added to `missing`, by its path from
|
|
781
|
+
* the folder.
|
|
782
|
+
*/
|
|
783
|
+
function withImports(files: Map<string, string>, entry: string, missing: string[] = []): string[] {
|
|
588
784
|
const seen = new Set<string>()
|
|
589
785
|
const visit = (path: string) => {
|
|
590
786
|
if (seen.has(path) || !files.has(path)) return
|
|
591
787
|
seen.add(path)
|
|
788
|
+
// A file kept from outside the folder imports by its own place there.
|
|
789
|
+
const here = fromOutsidePath(path) ?? path
|
|
592
790
|
for (const m of files.get(path)!.matchAll(/from\s+['"](\.{1,2}\/[^'"]+)['"]/g)) {
|
|
593
|
-
const base = posix.normalize(posix.join(posix.dirname(
|
|
594
|
-
|
|
791
|
+
const base = posix.normalize(posix.join(posix.dirname(here), m[1]!))
|
|
792
|
+
const candidates = [base, `${base}.ts`, `${base}/index.ts`, `${base}.js`, `${base}/index.js`].map((c) => (c.startsWith('../') ? outsidePath(c) : c))
|
|
793
|
+
const found = candidates.find((c) => files.has(c))
|
|
794
|
+
if (found) {
|
|
795
|
+
visit(found)
|
|
796
|
+
// A JavaScript file's types beside it (a module a pull recovered from a deployed bundle).
|
|
797
|
+
if (found.endsWith('.js')) visit(`${found.slice(0, -'.js'.length)}.d.ts`)
|
|
798
|
+
} else if (base.startsWith('../')) missing.push(base)
|
|
595
799
|
}
|
|
596
800
|
}
|
|
597
801
|
visit(entry)
|
|
@@ -622,6 +826,7 @@ async function readEditorDraft(source: SyncSource, row: FunctionRow, it: RemoteI
|
|
|
622
826
|
|
|
623
827
|
async function readFunctions(source: SyncSource, lists: Lists, opts: ReadOptions, warnings: string[]): Promise<RemoteItem[]> {
|
|
624
828
|
const out: RemoteItem[] = []
|
|
829
|
+
const missingImports: MissingImports[] = []
|
|
625
830
|
for (const row of lists.functions) {
|
|
626
831
|
const ref = `function/${row.slug}`
|
|
627
832
|
if (!wanted(opts, ref)) continue
|
|
@@ -672,15 +877,50 @@ async function readFunctions(source: SyncSource, lists: Lists, opts: ReadOptions
|
|
|
672
877
|
warnings.push(`Function ${row.slug}: imports ${list(outside)}, which a pull does not write into a folder of the project’s own items`)
|
|
673
878
|
}
|
|
674
879
|
} else {
|
|
675
|
-
// A Function deployed from its own folder:
|
|
676
|
-
|
|
880
|
+
// A Function deployed from its own folder: its entry, what that imports, its test and the
|
|
881
|
+
// folder's project files, under functions/<slug>/; never its siblings or the SDK copy.
|
|
882
|
+
const folder = folderFunctionFiles(files, row.slug)
|
|
883
|
+
for (const [p, content] of Object.entries(folder.files)) it.files[`functions/${row.slug}/${p}`] = content
|
|
677
884
|
if (!Object.keys(it.files).length) continue
|
|
678
885
|
it.notes.push('Deployed from its own folder; pulled as functions/' + row.slug + '/')
|
|
886
|
+
// What it imports from beside its folder, written where the import finds it: a shared file of the
|
|
887
|
+
// solution (`functions/shared/…`, `lib/…`), never inside another Function's folder or above the project.
|
|
888
|
+
const folders = new Set(lists.functions.map((f) => f.slug))
|
|
889
|
+
const beside = (fromFolder: string) => {
|
|
890
|
+
const path = posix.normalize(posix.join('functions', row.slug, fromFolder))
|
|
891
|
+
const parts = path.split('/')
|
|
892
|
+
if (path.startsWith('../') || parts.some((p) => p.startsWith('.'))) return null
|
|
893
|
+
if (parts[0] === 'functions') return parts.length >= 3 && !folders.has(parts[1]!) ? path : null
|
|
894
|
+
return isSharedFunctionFile(path) ? path : null
|
|
895
|
+
}
|
|
896
|
+
const brought: string[] = []
|
|
897
|
+
for (const [rel, content] of Object.entries(folder.outside)) {
|
|
898
|
+
const path = beside(rel)
|
|
899
|
+
if (!path) continue
|
|
900
|
+
it.files[path] = content
|
|
901
|
+
brought.push(path)
|
|
902
|
+
}
|
|
903
|
+
const list = (paths: string[]) => (paths.length > 3 ? `${paths.slice(0, 3).join(', ')} and ${paths.length - 3} more` : paths.length > 1 ? `${paths.slice(0, -1).join(', ')} and ${paths[paths.length - 1]}` : paths.join(''))
|
|
904
|
+
if (brought.length) it.notes.push(`Brings ${list(brought.sort())}, which it imports from beside its folder, so it builds as in the workspace`)
|
|
905
|
+
// What its source lacks: from another Function's source, else cut back out of its deployed
|
|
906
|
+
// bundle once every Function is read (`recoverMissingImports`), else said.
|
|
907
|
+
if (folder.missing.length && folder.entry) {
|
|
908
|
+
missingImports.push({ it, slug: row.slug, version: row.liveVersion, files, entry: folder.entry, missing: folder.missing, writable: (path) => beside(posix.relative(posix.join('functions', row.slug), `${path}.js`)) === `${path}.js` })
|
|
909
|
+
} else if (folder.missing.length) {
|
|
910
|
+
const lacking = folder.missing.map((m) => posix.normalize(posix.join('functions', row.slug, m)))
|
|
911
|
+
it.notes.push(missingImportNote(lacking))
|
|
912
|
+
warnings.push(missingImportWarning(row.slug, lacking))
|
|
913
|
+
}
|
|
914
|
+
if (!folder.entry) {
|
|
915
|
+
it.notes.push('Its source names no entry for it, so every file of its folder was pulled')
|
|
916
|
+
warnings.push(`Function ${row.slug}: no file in its source declares \`automation({ name: '${row.slug}' })\`; its whole folder was pulled`)
|
|
917
|
+
}
|
|
679
918
|
}
|
|
680
919
|
// Functions name other resources only in their code, if at all; nothing to rewrite.
|
|
681
920
|
it.requires = []
|
|
682
921
|
out.push(it)
|
|
683
922
|
}
|
|
923
|
+
if (missingImports.length) await recoverMissingImports(missingImports, out, (path) => source.workspace.getBytes(path), warnings)
|
|
684
924
|
noteSharedFunctionFiles(out, warnings)
|
|
685
925
|
return out
|
|
686
926
|
}
|
|
@@ -771,6 +1011,13 @@ async function readSkills(source: SyncSource, lists: Lists, opts: ReadOptions, w
|
|
|
771
1011
|
return out
|
|
772
1012
|
}
|
|
773
1013
|
|
|
1014
|
+
/** A number a file holds as one: `"0.5"` (a decimal the platform answers as text) is 0.5; anything else is left out or kept as it is. */
|
|
1015
|
+
export function fileNumber(value: unknown): number | undefined {
|
|
1016
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined
|
|
1017
|
+
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) return Number(value)
|
|
1018
|
+
return value === null || value === undefined ? undefined : (value as number)
|
|
1019
|
+
}
|
|
1020
|
+
|
|
774
1021
|
async function readKnowledge(source: SyncSource, lists: Lists, opts: ReadOptions, warnings: string[]): Promise<RemoteItem[]> {
|
|
775
1022
|
const out: RemoteItem[] = []
|
|
776
1023
|
for (const k of lists.knowledge.filter((x) => wanted(opts, `knowledge/${refName(x.name)}`))) {
|
|
@@ -780,17 +1027,18 @@ async function readKnowledge(source: SyncSource, lists: Lists, opts: ReadOptions
|
|
|
780
1027
|
description: k.description ?? undefined,
|
|
781
1028
|
retrieval: k.retrieval ?? undefined,
|
|
782
1029
|
chunker: k.chunker ?? undefined,
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1030
|
+
// Numbers as the file format has them: the platform reads a decimal column back as text ("0.5").
|
|
1031
|
+
chunkSize: fileNumber(k.chunkSize),
|
|
1032
|
+
chunkOverlap: fileNumber(k.chunkOverlap),
|
|
1033
|
+
similarityThreshold: fileNumber(k.similarityThreshold),
|
|
1034
|
+
topK: fileNumber(k.topK),
|
|
787
1035
|
// How it searches and reads documents: hybrid search and reranking, extraction, OCR.
|
|
788
1036
|
hybridConfig: k.hybridConfig ?? undefined,
|
|
789
1037
|
extractorConfig: k.extractorConfig ?? undefined,
|
|
790
1038
|
ocrConfig: k.ocrConfig ?? undefined,
|
|
791
1039
|
}).filter(([, v]) => v !== undefined))
|
|
792
1040
|
const it = item('knowledge', name, k.name, { [`knowledge/${name}/knowledge.json`]: json(doc) }, {
|
|
793
|
-
notes: ['Its documents
|
|
1041
|
+
notes: ['Its documents come with it when it is pulled (unless `--no-documents`), and `local dev` uploads them; Markdown or text files added to this folder are uploaded too'],
|
|
794
1042
|
requires: [],
|
|
795
1043
|
id: k.id,
|
|
796
1044
|
})
|
|
@@ -898,7 +1146,36 @@ async function readPlugins(lists: Lists, opts: ReadOptions, tools: PluginTools):
|
|
|
898
1146
|
return out
|
|
899
1147
|
}
|
|
900
1148
|
|
|
901
|
-
interface BundleAnswer {
|
|
1149
|
+
interface BundleAnswer {
|
|
1150
|
+
definition: Parameters<typeof toFiles>[0] | null
|
|
1151
|
+
datasetNames: Record<string, string>
|
|
1152
|
+
/** The workspace's own grants beyond read (the workspace's slice only): `blueprint/workspace-grants.yaml`. */
|
|
1153
|
+
workspaceGrants?: Array<{ objectType?: unknown; permissions?: unknown }>
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* `blueprint/workspace-grants.yaml` as the workspace's slice answers it: the
|
|
1158
|
+
* types the workspace holds more than `read` on (today, `read_sensitive`), in
|
|
1159
|
+
* the file's own shape (`local-workspace-grants.ts`). None: no item, so a
|
|
1160
|
+
* pull writes no file.
|
|
1161
|
+
*/
|
|
1162
|
+
export function workspaceGrantsItem(grants: BundleAnswer['workspaceGrants']): RemoteItem | null {
|
|
1163
|
+
const entries = (Array.isArray(grants) ? grants : []).flatMap((g) => {
|
|
1164
|
+
const type = typeof g?.objectType === 'string' ? g.objectType.replace(/^objectType\//, '') : ''
|
|
1165
|
+
const permissions = Array.isArray(g?.permissions) ? g.permissions.filter((p): p is string => typeof p === 'string') : []
|
|
1166
|
+
return type && permissions.length ? [{ objectType: `objectType/${type}`, permissions }] : []
|
|
1167
|
+
})
|
|
1168
|
+
if (!entries.length) return null
|
|
1169
|
+
const doc = new YamlDocument({ grants: entries })
|
|
1170
|
+
// `permissions: [read, read_sensitive]`, as the file is written by hand.
|
|
1171
|
+
for (const entry of (doc.get('grants') as YAMLSeq<YAMLMap>).items) {
|
|
1172
|
+
const permissions = entry.get('permissions', true)
|
|
1173
|
+
if (isSeq(permissions)) permissions.flow = true
|
|
1174
|
+
}
|
|
1175
|
+
const it = item('blueprint', WORKSPACE_GRANTS_NAME, 'Workspace grants', { [WORKSPACE_GRANTS_FILE]: doc.toString({ lineWidth: 0, flowCollectionPadding: false }) })
|
|
1176
|
+
it.requires = entries.map((e) => itemRef('objectType', e.objectType.slice('objectType/'.length))).sort()
|
|
1177
|
+
return it
|
|
1178
|
+
}
|
|
902
1179
|
|
|
903
1180
|
/**
|
|
904
1181
|
* The saved object sets as Blueprint files (`blueprint/object-sets/<name>.yaml`):
|
|
@@ -971,6 +1248,8 @@ async function readBlueprint(
|
|
|
971
1248
|
let answer: BundleAnswer | null = null
|
|
972
1249
|
// The draft read in place of the release, already holding its Actions.
|
|
973
1250
|
let fromDraft = false
|
|
1251
|
+
// The workspace's grants beyond read, from its slice (the organization's model has none of a workspace's).
|
|
1252
|
+
let grants: RemoteItem | null = null
|
|
974
1253
|
if (mode === 'organization' && source.organization) {
|
|
975
1254
|
const lifecycle = one<Lifecycle>(await source.organization.get('/v1/blueprint/lifecycle'))
|
|
976
1255
|
fromDraft = !!lifecycle.draft?.definition && (!!opts.draft || !lifecycle.activeRelease?.definition)
|
|
@@ -978,8 +1257,10 @@ async function readBlueprint(
|
|
|
978
1257
|
if (fromDraft) notes.push(opts.draft ? 'The Blueprint was read from its unpublished draft, as asked' : 'The Blueprint has no published release; its draft was read')
|
|
979
1258
|
} else {
|
|
980
1259
|
const sliced = await source.workspace.find<unknown>('/v1/sync/blueprint')
|
|
981
|
-
if (sliced)
|
|
982
|
-
|
|
1260
|
+
if (sliced) {
|
|
1261
|
+
answer = one<BundleAnswer>(sliced)
|
|
1262
|
+
grants = workspaceGrantsItem(answer.workspaceGrants)
|
|
1263
|
+
} else if (source.organization) {
|
|
983
1264
|
notes.push('This platform cannot cut the Blueprint to the workspace yet; the organization key read the whole model')
|
|
984
1265
|
return readBlueprint(source, 'organization', warnings, notes, sets, opts)
|
|
985
1266
|
} else {
|
|
@@ -994,7 +1275,7 @@ async function readBlueprint(
|
|
|
994
1275
|
if (lifecycle.draft?.definition) {
|
|
995
1276
|
answer = { definition: sliceDraft(lifecycle.draft.definition, granted), datasetNames: { ...(answer.datasetNames ?? {}), ...(await datasetNames(source.organization)) } }
|
|
996
1277
|
fromDraft = true
|
|
997
|
-
notes.push('The Blueprint was read from its unpublished draft, as asked, cut to the object types
|
|
1278
|
+
notes.push('The Blueprint was read from its unpublished draft, as asked, cut to the object types the workspace is granted')
|
|
998
1279
|
}
|
|
999
1280
|
}
|
|
1000
1281
|
}
|
|
@@ -1003,6 +1284,8 @@ async function readBlueprint(
|
|
|
1003
1284
|
// With the organization key, the published Actions on the slice's object types come along.
|
|
1004
1285
|
const definition = mode === 'workspace' && source.organization && !fromDraft ? await withActions(source.organization, answer.definition, warnings, notes) : answer.definition
|
|
1005
1286
|
const model = toFiles(definition, new Map(Object.entries(answer.datasetNames ?? {})))
|
|
1287
|
+
// What an Action is checked against, as the local runtime checks it when `local dev` writes it.
|
|
1288
|
+
const semantic = semanticFactsOf(model.files.map((f) => ({ path: f.path, document: f.document as Record<string, unknown> })))
|
|
1006
1289
|
const objectTypes = new Set(model.files.flatMap((f) => (f.path.startsWith('object-types/') ? [f.apiName] : [])))
|
|
1007
1290
|
const saved = objectSetItems(sets.lists.objectSets ?? [], sets.directory, objectTypes)
|
|
1008
1291
|
return [...model.files.map((file) => {
|
|
@@ -1013,12 +1296,20 @@ async function readBlueprint(
|
|
|
1013
1296
|
if (file.path.startsWith('actions/')) {
|
|
1014
1297
|
// An Action brings the object types it acts on, and says what it does on the local runtime.
|
|
1015
1298
|
const doc = file.document as Record<string, any>
|
|
1016
|
-
const fit = actionFit(doc)
|
|
1299
|
+
const fit = actionFit(doc, semantic)
|
|
1017
1300
|
if (fit.subject) it.requires = [itemRef('objectType', fit.subject)]
|
|
1018
|
-
it.notes.push(
|
|
1301
|
+
it.notes.push(
|
|
1302
|
+
fit.refused
|
|
1303
|
+
? `Won’t run on the local runtime: ${fit.reason!.replace(/^the local runtime refuses it: /, 'it refuses it as written. ')} The fix is the Blueprint author’s, in the workspace: change the Action or the semantic type so they agree, then pull it`
|
|
1304
|
+
: fit.runsLocally
|
|
1305
|
+
? `Deployed on the local runtime when it applies: it writes ${fit.subject ?? 'its object'}’s records there`
|
|
1306
|
+
: `Published switched off on the local runtime: ${fit.reason}`,
|
|
1307
|
+
)
|
|
1308
|
+
// As the workspace runs it, a value it offers that the type it writes refuses: inherited, not refused here.
|
|
1309
|
+
if (fit.inheritedWarnings?.length) it.notes.push(`Runs with a warning inherited from the workspace: ${fit.inheritedWarnings.join(' ')} A request with such a value is refused when it is submitted, here as in the workspace; the Blueprint author fixes the Action’s contract there, then you pull it`)
|
|
1019
1310
|
}
|
|
1020
1311
|
return it
|
|
1021
|
-
}), ...saved]
|
|
1312
|
+
}), ...saved, ...(grants ? [grants] : [])]
|
|
1022
1313
|
}
|
|
1023
1314
|
|
|
1024
1315
|
type Bundle = NonNullable<BundleAnswer['definition']>
|
|
@@ -1048,7 +1339,7 @@ async function withActions(org: SyncClient, sliced: Bundle, warnings: string[],
|
|
|
1048
1339
|
if (touched.every((id) => ids.has(id))) kept.push(action)
|
|
1049
1340
|
else outside.push(String(action.apiName))
|
|
1050
1341
|
}
|
|
1051
|
-
if (outside.length) notes.push(`${outside.length} governed Action${outside.length === 1 ? ' acts' : 's act'} on object types
|
|
1342
|
+
if (outside.length) notes.push(`${outside.length} governed Action${outside.length === 1 ? ' acts' : 's act'} on object types the workspace isn’t granted, so ${outside.length === 1 ? 'it stays' : 'they stay'} out: ${outside.join(', ')}`)
|
|
1052
1343
|
return { ...def, actions: kept } as Bundle
|
|
1053
1344
|
}
|
|
1054
1345
|
|
|
@@ -1375,9 +1666,29 @@ export async function readWorkspace(source: SyncSource, opts: ReadOptions = {}):
|
|
|
1375
1666
|
noteConnections(items, lists)
|
|
1376
1667
|
noteActions(items, directory)
|
|
1377
1668
|
// What each item needs against what the local runtime can do (§8), with the Actions this read brought.
|
|
1378
|
-
|
|
1669
|
+
// The checks `local dev` will apply, run here so the index says what won't run before anything is applied:
|
|
1670
|
+
// an Action's values against the semantic types it writes, and the files a Function's code imports.
|
|
1671
|
+
const semantic = semanticFactsOf(items.filter((i) => i.kind === 'blueprint').flatMap((i) => Object.entries(i.files).flatMap(([path, text]) => {
|
|
1672
|
+
try {
|
|
1673
|
+
const document = parseYaml(text)
|
|
1674
|
+
return document && typeof document === 'object' ? [{ path, document: document as Record<string, unknown> }] : []
|
|
1675
|
+
} catch {
|
|
1676
|
+
return []
|
|
1677
|
+
}
|
|
1678
|
+
})))
|
|
1679
|
+
const actions = new Map(items.filter((i) => i.kind === 'blueprint' && i.name.startsWith('actions/')).flatMap((i) => Object.values(i.files).map((text) => [i.name.slice('actions/'.length), actionFit((parseYaml(text) ?? {}) as Record<string, unknown>, semantic)] as const)))
|
|
1379
1680
|
const titles = new Map(lists.apps.flatMap((a) => (a.install && a.catalog?.displayName ? [[a.install.installName, a.catalog.displayName] as const] : [])))
|
|
1380
|
-
|
|
1681
|
+
const pulledFiles = new Set(items.flatMap((i) => Object.keys(i.files)))
|
|
1682
|
+
const hasFile = (path: string) => pulledFiles.has(path)
|
|
1683
|
+
for (const it of items) it.portability = portabilityOf(it.kind, it.files, { connections: connectionNames(lists), builtIns: builtInKinds(lists), context: { actions, titles }, semantic, hasFile })
|
|
1684
|
+
// A Function's code that names an object type by the workspace's id: the local runtime's ids differ.
|
|
1685
|
+
const platformIds = platformObjectTypeIds(directory)
|
|
1686
|
+
for (const it of items) {
|
|
1687
|
+
if (it.kind !== 'function') continue
|
|
1688
|
+
const hits = hardCodedObjectTypeIds(it.files, platformIds)
|
|
1689
|
+
if (!hits.length) continue
|
|
1690
|
+
it.notes.push(`Hard-codes the workspace’s object type ids, which the local runtime doesn’t share: ${hits.map((h) => `${h.file}:${h.line} (${h.objectType})`).join(', ')}. \`local dev\` deploys it here with this runtime’s ids in their place and leaves the files as they are; take the id from the query result (\`subjectOf\`, or \`result.objectTypeId\`) so the code holds on any runtime`)
|
|
1691
|
+
}
|
|
1381
1692
|
if (!source.organization && items.some((i) => i.unresolved.length)) {
|
|
1382
1693
|
notes.push(directory.model?.size
|
|
1383
1694
|
? 'Governed Actions are named only with an organization key (they are organization-wide); an Action step shows as an id until you connect one'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
|
|
4
|
+
import { ignoreInFronteraDir } from '../project-context'
|
|
4
5
|
import { CliError } from '../errors'
|
|
5
6
|
import { writeFileAtomic } from '../fs-atomic'
|
|
6
7
|
import { hash, itemHash, safePath, SYNC_KINDS, type RemoteItem, type SyncKind, type SyncLock } from './local-sync-plan'
|
|
@@ -53,6 +54,8 @@ export interface SnapshotItem {
|
|
|
53
54
|
state?: RemoteItem['state']
|
|
54
55
|
/** Its binary files (a skill's assets), by project path, kept under `.frontera/sync/remote-assets/`. */
|
|
55
56
|
assets?: string[]
|
|
57
|
+
/** Its files recovered from the deployed bundle, not its source (`RemoteItem.recovered`). */
|
|
58
|
+
recovered?: string[]
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
export interface RemoteSnapshot {
|
|
@@ -122,9 +125,14 @@ export interface RemoteSnapshot {
|
|
|
122
125
|
* 10: a Plugin's description and icon. 11: Custom Apps, the brand and the
|
|
123
126
|
* library. 12: an Agent's object tools carry their object definitions,
|
|
124
127
|
* named by slug (`object/<slug>`). 13: a knowledge base's quality checks
|
|
125
|
-
* (`quality-checks.yaml`).
|
|
128
|
+
* (`quality-checks.yaml`). 14: a Function's imports its source lacks,
|
|
129
|
+
* recovered from its deployed bundle. 15: an Agent's behavior settings read
|
|
130
|
+
* from its snapshot or record, as a released platform gives them. 16: an
|
|
131
|
+
* item's portability runs the checks `local dev` applies: a Function whose
|
|
132
|
+
* imports the pull can't bring, and an Action whose values the semantic types
|
|
133
|
+
* it writes refuse, are said not to run here.
|
|
126
134
|
*/
|
|
127
|
-
export const SNAPSHOT_READER =
|
|
135
|
+
export const SNAPSHOT_READER = 16
|
|
128
136
|
|
|
129
137
|
/** The directory the snapshot recorded, as the reference rewriter reads it. */
|
|
130
138
|
export function directoryOf(snap: RemoteSnapshot | null): Directory | undefined {
|
|
@@ -182,7 +190,7 @@ export function snapshotItems(root: string, snap: RemoteSnapshot): RemoteItem[]
|
|
|
182
190
|
files[path] = content
|
|
183
191
|
}
|
|
184
192
|
const assets = (s.assets ?? []).map((path) => ({ path, from: underDir(root, ASSETS_DIR, path) }))
|
|
185
|
-
return [{ ref, kind: s.kind, name: s.name, title: s.title, files, version: s.version, id: s.id, revision: s.revision, unresolved: s.unresolved, notes: s.notes, requires: s.requires, ...(s.portability ? { portability: s.portability } : {}), ...(s.state ? { state: s.state } : {}), ...(assets.length ? { assets } : {}) }]
|
|
193
|
+
return [{ ref, kind: s.kind, name: s.name, title: s.title, files, version: s.version, id: s.id, revision: s.revision, unresolved: s.unresolved, notes: s.notes, requires: s.requires, ...(s.portability ? { portability: s.portability } : {}), ...(s.state ? { state: s.state } : {}), ...(assets.length ? { assets } : {}), ...(s.recovered?.length ? { recovered: s.recovered } : {}) }]
|
|
186
194
|
})
|
|
187
195
|
}
|
|
188
196
|
|
|
@@ -208,12 +216,8 @@ export function knownGone(snap: RemoteSnapshot | null): (ref: string) => boolean
|
|
|
208
216
|
|
|
209
217
|
/** `.frontera/` keeps the runtime's state out of git; the snapshot and the base stay out too. */
|
|
210
218
|
function ignoreSyncDir(root: string): void {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const text = existsSync(ignore) ? readFileSync(ignore, 'utf8') : ''
|
|
214
|
-
if (text.split(/\r?\n/).some((l) => l.trim() === 'sync/' || l.trim() === 'sync')) return
|
|
215
|
-
if (text) appendFileSync(ignore, `${text.endsWith('\n') ? '' : '\n'}sync/\n`)
|
|
216
|
-
else writeFileSync(ignore, 'local/\nsync/\n')
|
|
219
|
+
// A pulled or linked project: its sync state, and this machine's profile binding, stay out of git.
|
|
220
|
+
ignoreInFronteraDir(root, ['local/', 'sync/'])
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
function toSnapshotItem(item: RemoteItem, revision?: string): SnapshotItem {
|
|
@@ -232,6 +236,7 @@ function toSnapshotItem(item: RemoteItem, revision?: string): SnapshotItem {
|
|
|
232
236
|
...(item.portability ? { portability: item.portability } : {}),
|
|
233
237
|
...(item.state ? { state: item.state } : {}),
|
|
234
238
|
...(item.assets?.length ? { assets: item.assets.map((a) => a.path).sort() } : {}),
|
|
239
|
+
...(item.recovered?.length ? { recovered: [...item.recovered].sort() } : {}),
|
|
235
240
|
}
|
|
236
241
|
}
|
|
237
242
|
|