primitive-admin 1.2.0-alpha.6 → 1.2.0-alpha.8
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 -3
- package/assets/skill/skills/primitive-platform/SKILL.md +12 -6
- package/dist/src/commands/apps-children.d.ts +1 -1
- package/dist/src/commands/apps-children.js +172 -28
- package/dist/src/commands/apps-children.js.map +1 -1
- package/dist/src/commands/auth.js +147 -38
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/config.js +43 -4
- package/dist/src/commands/config.js.map +1 -1
- package/dist/src/commands/database-types.js +313 -13
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.js +143 -54
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +70 -11
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/init.d.ts +7 -0
- package/dist/src/commands/init.js +9 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/llm.js +6 -9
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/settings.d.ts +9 -12
- package/dist/src/commands/settings.js +79 -31
- package/dist/src/commands/settings.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +12 -2
- package/dist/src/commands/sync-app-settings.js +35 -10
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +118 -6
- package/dist/src/commands/sync.js +638 -65
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +25 -6
- package/dist/src/lib/api-client.js +106 -72
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +38 -0
- package/dist/src/lib/app-settings-descriptor.js +96 -4
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/child-apps-local.d.ts +45 -21
- package/dist/src/lib/child-apps-local.js +68 -30
- package/dist/src/lib/child-apps-local.js.map +1 -1
- package/dist/src/lib/credentials-home.d.ts +51 -0
- package/dist/src/lib/credentials-home.js +169 -0
- package/dist/src/lib/credentials-home.js.map +1 -0
- package/dist/src/lib/credentials-lock.d.ts +80 -0
- package/dist/src/lib/credentials-lock.js +222 -0
- package/dist/src/lib/credentials-lock.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +92 -5
- package/dist/src/lib/credentials-store.js +377 -53
- package/dist/src/lib/credentials-store.js.map +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +1 -1
- package/dist/src/lib/env-resolver-core.js.map +1 -1
- package/dist/src/lib/function-bundle.d.ts +38 -18
- package/dist/src/lib/function-bundle.js +60 -35
- package/dist/src/lib/function-bundle.js.map +1 -1
- package/dist/src/lib/function-collect.d.ts +1 -1
- package/dist/src/lib/function-collect.js +4 -0
- package/dist/src/lib/function-collect.js.map +1 -1
- package/dist/src/lib/function-document-types.d.ts +5 -3
- package/dist/src/lib/function-document-types.js +10 -14
- package/dist/src/lib/function-document-types.js.map +1 -1
- package/dist/src/lib/function-source-roots.d.ts +67 -0
- package/dist/src/lib/function-source-roots.js +148 -0
- package/dist/src/lib/function-source-roots.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +27 -0
- package/dist/src/lib/function-sync.js +16 -3
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/function-triggers.d.ts +3 -15
- package/dist/src/lib/function-triggers.js +10 -72
- package/dist/src/lib/function-triggers.js.map +1 -1
- package/dist/src/lib/function-typecheck.d.ts +37 -2
- package/dist/src/lib/function-typecheck.js +210 -28
- package/dist/src/lib/function-typecheck.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +148 -0
- package/dist/src/lib/generated-config-surfaces.js +229 -35
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/local-state.d.ts +21 -6
- package/dist/src/lib/local-state.js +78 -34
- package/dist/src/lib/local-state.js.map +1 -1
- package/dist/src/lib/login-scope-lines.d.ts +1 -1
- package/dist/src/lib/login-scope-lines.js +1 -1
- package/dist/src/lib/login-scope-lines.js.map +1 -1
- package/dist/src/lib/project-config.d.ts +7 -0
- package/dist/src/lib/project-config.js +22 -1
- package/dist/src/lib/project-config.js.map +1 -1
- package/dist/src/lib/push-failure-report.d.ts +45 -0
- package/dist/src/lib/push-failure-report.js +57 -0
- package/dist/src/lib/push-failure-report.js.map +1 -0
- package/dist/src/lib/root-import-decision.d.ts +9 -4
- package/dist/src/lib/root-import-decision.js +24 -3
- package/dist/src/lib/root-import-decision.js.map +1 -1
- package/dist/src/lib/scope-request.d.ts +8 -5
- package/dist/src/lib/scope-request.js +17 -8
- package/dist/src/lib/scope-request.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +5 -1
- package/dist/src/lib/sync-selectors.js +23 -13
- package/dist/src/lib/sync-selectors.js.map +1 -1
- package/dist/src/lib/transient-retry.d.ts +9 -0
- package/dist/src/lib/transient-retry.js +37 -0
- package/dist/src/lib/transient-retry.js.map +1 -1
- package/dist/src/lib/vars-override.d.ts +105 -0
- package/dist/src/lib/vars-override.js +143 -0
- package/dist/src/lib/vars-override.js.map +1 -0
- package/package.json +2 -2
- package/dist/src/lib/generated-workflow-model-fields.d.ts +0 -20
- package/dist/src/lib/generated-workflow-model-fields.js +0 -53
- package/dist/src/lib/generated-workflow-model-fields.js.map +0 -1
- package/dist/src/lib/workflow-field-descriptor.d.ts +0 -91
- package/dist/src/lib/workflow-field-descriptor.js +0 -153
- package/dist/src/lib/workflow-field-descriptor.js.map +0 -1
|
@@ -13,6 +13,9 @@ import { parseMetadataCategoryToml, validateMetadataCategoryConfig, serializeMet
|
|
|
13
13
|
import { isPlatformOwnedWorkflow, isPlatformOwnedWorkflowKey, } from "../lib/platform-owned.js";
|
|
14
14
|
import { getCurrentAppId, getServerUrl } from "../lib/config.js";
|
|
15
15
|
import { requireCurrentEnvironment } from "../lib/env-resolver.js";
|
|
16
|
+
import { ensureFileIgnored } from "../lib/local-state.js";
|
|
17
|
+
import { PROJECT_TREE_DIR } from "../lib/env-resolver-core.js";
|
|
18
|
+
import { cachedVarsNote, layerVars, planVarsOverridePull, varsOverridePath as resolveVarsOverridePath, varsOverrideSummaryLine, VARS_OVERRIDE_GITIGNORE_PATTERN, } from "../lib/vars-override.js";
|
|
16
19
|
import { resolveSyncDir, resolveSnapshotsRoot, resolveSyncStateReadPath, resolveSyncStateWritePath, SYNC_STATE_FILENAME, checkLegacyDatabaseTypesDir, } from "../lib/sync-paths.js";
|
|
17
20
|
import { createSnapshot, listSnapshots, resolveSnapshot, restoreSnapshot, pruneSnapshots, } from "../lib/snapshots.js";
|
|
18
21
|
import { validateWorkflowToml, collectWorkflowTomlNotes, formatWorkflowTomlErrors, validateWorkflowExpressionsToml, validateWorkflowExpressionsRawToml, } from "../lib/workflow-toml-validator.js";
|
|
@@ -23,7 +26,7 @@ import { canonicalizeServerTextFields } from "../lib/server-text-normalization.j
|
|
|
23
26
|
import { canonicalJsonStringify } from "../lib/canonical-json.js";
|
|
24
27
|
import { reconcileWorkflowIncludes } from "../lib/workflow-include-preserve.js";
|
|
25
28
|
import { newPullWriteTally, writePulledFile, } from "../lib/pull-write.js";
|
|
26
|
-
import { serializeSection, parseTomlToAppSettings, buildAppSettingsPayload, } from "../lib/app-settings-descriptor.js";
|
|
29
|
+
import { serializeSection, parseTomlToAppSettings, buildAppSettingsPayload, settingsProtectionLevel, } from "../lib/app-settings-descriptor.js";
|
|
27
30
|
import { collectAppSettingsPushErrors, writePulledAppSettings, diffAppSettings, appSettingsFieldDiffs, appSettingsNameExcluded, appSettingsNameExcludedForState, hashAppSettingsForDiff, } from "./sync-app-settings.js";
|
|
28
31
|
import { applyWorkflowBody, legacyDraftSlotWarning } from "../lib/workflow-apply.js";
|
|
29
32
|
// #2645 — named config bodies live in `workflows/<key>.configs/<name>.toml`.
|
|
@@ -48,12 +51,15 @@ import { AGENT_TEST_CASE_FIELDS, resolveAgentTestCase, } from "../lib/generated-
|
|
|
48
51
|
// #3799 — the meaning-preserving schema translation, vendored the same way, so
|
|
49
52
|
// the preflight refuses exactly the schemas the admin routes refuse.
|
|
50
53
|
import { agentSchemaTranslationRefusals } from "../lib/generated-config-surfaces.js";
|
|
54
|
+
// #4441 — the rule for a change to a function an agent uses, vendored the same
|
|
55
|
+
// way, so the preflight refuses exactly the changes the function routes refuse.
|
|
56
|
+
import { agentFunctionChangeRefusals, agentFunctionUses, } from "../lib/generated-config-surfaces.js";
|
|
51
57
|
// #3801 — the strict-output rule, vendored the same way, so the preflight
|
|
52
58
|
// refuses exactly the strict configs the admin routes refuse.
|
|
53
59
|
import { resolvePromptStrictOutput } from "../lib/generated-config-surfaces.js";
|
|
54
60
|
// #3179 — the function's two halves: the header payloads and the envelope
|
|
55
61
|
// `config push` sends, and the bytes `config pull` writes back.
|
|
56
|
-
import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
62
|
+
import { applyFunctionPull, buildFunctionConfigPush, buildFunctionConfigPushDetailed, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
57
63
|
import { newerVersionLine, pullNewerVersionLine, rollbackDiffHint, } from "../lib/function-versions.js";
|
|
58
64
|
import { FunctionBuildError } from "../lib/function-bundle.js";
|
|
59
65
|
// #3182 — the local half of capability validation. The grammar is the vendored
|
|
@@ -71,6 +77,7 @@ import { isFunctionDeclarationFile, resolveDocumentSchema, } from "../lib/functi
|
|
|
71
77
|
// #3331 — and the compiler is actually asked: a push holds the code it ships
|
|
72
78
|
// to the declarations it just generated.
|
|
73
79
|
import { createFunctionTypechecker, } from "../lib/function-typecheck.js";
|
|
80
|
+
import { resolveFunctionSourceRoots } from "../lib/function-source-roots.js";
|
|
74
81
|
import { detectLayout, migrateLegacyToV2 } from "../lib/block-layout.js";
|
|
75
82
|
import { ABSENT_FROM_EXPORT, LOCAL_ONLY_ABSENT, LOCAL_ONLY_NEW, PUSH_PRUNE_SPECS, REMOTE_ONLY_UNMANAGED, REMOTE_ONLY_WILL_DELETE, SYNC_RESOURCE_TYPES, VALIDATION_ERROR, applyPrune, attachmentWritePath, attemptFetch, classifyLocalOnly, classifyPruneDeleteFailure, classifyRemoteOnly, collectPushPruneCandidates, decidePushPruneCandidate, formatPrunePlan, hasUncommittedChanges, isSafeAttachmentFilename, isSafeSyncFilename, planPrune, preserveUnwrittenEntries, removePrunedSidecar, ruleSetFileKey, } from "../lib/sync-resource-types.js";
|
|
76
83
|
import { normalizeTestCaseKey, testCaseKeyError, } from "../lib/test-case-keys.js";
|
|
@@ -82,6 +89,8 @@ export { resolveSlugCollisions, slugifyTestCaseName, testCaseFileBasenames, } fr
|
|
|
82
89
|
import { success, error, printApiError, info, warn, keyValue, json, divider, } from "../lib/output.js";
|
|
83
90
|
import { filterFilesBySelection, parseSyncSelectors, SelectorError, selectionIncludesKey, selectionIncludesType, selectionIncludesApp, selectionIncludesAnyVar, trackedSelectorKeys, unmatchedSelectors, } from "../lib/sync-selectors.js";
|
|
84
91
|
import { confirmPrompt, ConfirmPromptError } from "../lib/confirm-prompt.js";
|
|
92
|
+
import { pushFailureIsTransient, writePushFailureReport } from "../lib/push-failure-report.js";
|
|
93
|
+
import { isTransientRequestFailure } from "../lib/transient-retry.js";
|
|
85
94
|
import { signingSecretStatusWarning, signingSecretHealthWarning, previousSigningSecretWarning, } from "../lib/signing-secret-status.js";
|
|
86
95
|
import chalk from "chalk";
|
|
87
96
|
function ensureDir(dirPath) {
|
|
@@ -152,7 +161,7 @@ export function wrapEntityError(err, action, kind, key) {
|
|
|
152
161
|
return new ApiError(wrappedMessage, err.statusCode, err.code, err.details);
|
|
153
162
|
}
|
|
154
163
|
// Non-ApiError (e.g. network failure, JSON parse error) — preserve cause.
|
|
155
|
-
const wrapped = new Error(wrappedMessage);
|
|
164
|
+
const wrapped = new Error(wrappedMessage, { cause: err });
|
|
156
165
|
return wrapped;
|
|
157
166
|
}
|
|
158
167
|
/**
|
|
@@ -570,6 +579,69 @@ export function planVarsPull(outcome, priorVars) {
|
|
|
570
579
|
}
|
|
571
580
|
return { write: true, content: serializeVars(outcome.vars), varEntities };
|
|
572
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* The var leg of `config pull` on a machine-local environment (#4485): the
|
|
584
|
+
* server's vars go into the environment's `vars.<name>.override.toml`, and
|
|
585
|
+
* `vars.toml` — the shared tree's, which merges to main — is never written.
|
|
586
|
+
* Each pulled key (every key, or `selectedKeys`) is resolved against
|
|
587
|
+
* `vars.toml` by `planVarsOverridePull`. The file is created only when a key
|
|
588
|
+
* ends up in it; an existing one is rewritten, possibly to its comment-only
|
|
589
|
+
* state. The baseline entries (`varEntities`) are filled exactly as a pull on
|
|
590
|
+
* a committed environment fills them. A failed fetch writes nothing and keeps
|
|
591
|
+
* the prior entries.
|
|
592
|
+
*/
|
|
593
|
+
function pullVarsIntoOverride(input) {
|
|
594
|
+
const { outcome, overridePath, configDir, env, selectedKeys, priorVars, varEntities } = input;
|
|
595
|
+
const overrideFile = basename(overridePath);
|
|
596
|
+
if (!outcome.ok) {
|
|
597
|
+
const fetchError = outcome.error;
|
|
598
|
+
Object.assign(varEntities, priorVars ?? {});
|
|
599
|
+
error(` Skipped ${overrideFile} — failed to fetch config vars: ${fetchError?.message ?? fetchError}. ` +
|
|
600
|
+
`Left the existing ${overrideFile} untouched.`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const readTable = (path) => {
|
|
604
|
+
if (!existsSync(path))
|
|
605
|
+
return {};
|
|
606
|
+
try {
|
|
607
|
+
return parseConfigToml(readFileSync(path, "utf-8"));
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
error(` Skipped ${overrideFile} — could not parse ${basename(path)}: ${err?.message || String(err)}`);
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
const base = readTable(join(configDir, "vars.toml"));
|
|
615
|
+
const current = readTable(overridePath);
|
|
616
|
+
if (base === null || current === null) {
|
|
617
|
+
Object.assign(varEntities, priorVars ?? {});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
Object.assign(varEntities, selectedKeys
|
|
621
|
+
? planScopedVarsPull(outcome.vars, selectedKeys, {}, priorVars).varEntities
|
|
622
|
+
: planVarsPull(outcome).varEntities);
|
|
623
|
+
const plan = planVarsOverridePull(outcome.vars, base, selectedKeys, current, env);
|
|
624
|
+
const keys = Object.keys(plan.vars).sort();
|
|
625
|
+
const exists = existsSync(overridePath);
|
|
626
|
+
if (!exists && keys.length === 0)
|
|
627
|
+
return;
|
|
628
|
+
if (!exists) {
|
|
629
|
+
const coverage = ensureFileIgnored(overridePath, { reason: "primitive config pull" });
|
|
630
|
+
if (coverage.status === "failed") {
|
|
631
|
+
warn(` Could not confirm ${overrideFile} is git-ignored: ${coverage.detail}. ` +
|
|
632
|
+
`Add "${VARS_OVERRIDE_GITIGNORE_PATTERN}" to your .gitignore — the file is machine-local and must never be committed.`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const written = writePulledFile(overridePath, plan.content, input.tally, sameTomlAs(plan.content));
|
|
636
|
+
if (written)
|
|
637
|
+
info(` Wrote ${overrideFile} (${keys.length} var${keys.length === 1 ? "" : "s"})`);
|
|
638
|
+
if (keys.length > 0) {
|
|
639
|
+
info(` ${varsOverrideSummaryLine(overrideFile, {
|
|
640
|
+
overridingKeys: keys.filter((key) => key in base),
|
|
641
|
+
addedKeys: keys.filter((key) => !(key in base)),
|
|
642
|
+
})}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
573
645
|
/**
|
|
574
646
|
* `config pull --only var/<KEY>` — the same plan, narrowed to the named keys
|
|
575
647
|
* (issue #2645).
|
|
@@ -710,27 +782,28 @@ export function validateWebhookCapForPush(localKeys, remoteKeys) {
|
|
|
710
782
|
export function validateVarsFile(parsedVars, options = {}) {
|
|
711
783
|
const errors = [];
|
|
712
784
|
const { onlyKeys } = options;
|
|
785
|
+
const fileName = options.fileName ?? "vars.toml";
|
|
713
786
|
const keyCount = Object.keys(parsedVars).length;
|
|
714
|
-
if (!onlyKeys && keyCount > MAX_VARS_PER_APP) {
|
|
715
|
-
errors.push(
|
|
787
|
+
if (!onlyKeys && !options.skipCountCap && keyCount > MAX_VARS_PER_APP) {
|
|
788
|
+
errors.push(`${fileName} declares ${keyCount} vars, exceeding the maximum of ${MAX_VARS_PER_APP} vars per app. Remove ${keyCount - MAX_VARS_PER_APP} entr${keyCount - MAX_VARS_PER_APP === 1 ? "y" : "ies"}.`);
|
|
716
789
|
}
|
|
717
790
|
for (const [key, rawValue] of Object.entries(parsedVars)) {
|
|
718
791
|
if (onlyKeys && !onlyKeys.has(key))
|
|
719
792
|
continue;
|
|
720
793
|
if (!VARS_KEY_RE.test(key)) {
|
|
721
|
-
errors.push(`Invalid var key "${key}" in
|
|
794
|
+
errors.push(`Invalid var key "${key}" in ${fileName}: must match ^[A-Z][A-Z0-9_]{0,63}$ (uppercase letters, digits, underscores; start with a letter; max 64 chars)`);
|
|
722
795
|
continue;
|
|
723
796
|
}
|
|
724
797
|
if (typeof rawValue !== "string") {
|
|
725
|
-
errors.push(`Var "${key}" in
|
|
798
|
+
errors.push(`Var "${key}" in ${fileName} must be a string value (got ${typeof rawValue}) — quote the value.`);
|
|
726
799
|
continue;
|
|
727
800
|
}
|
|
728
801
|
const byteLength = new TextEncoder().encode(rawValue).length;
|
|
729
802
|
if (byteLength === 0) {
|
|
730
|
-
errors.push(`Var "${key}" in
|
|
803
|
+
errors.push(`Var "${key}" in ${fileName} must not be empty.`);
|
|
731
804
|
}
|
|
732
805
|
else if (byteLength > MAX_VAR_VALUE_BYTES) {
|
|
733
|
-
errors.push(`Var "${key}" in
|
|
806
|
+
errors.push(`Var "${key}" in ${fileName} exceeds the maximum value size of ${MAX_VAR_VALUE_BYTES} bytes (got ${byteLength}).`);
|
|
734
807
|
}
|
|
735
808
|
}
|
|
736
809
|
return errors;
|
|
@@ -943,7 +1016,12 @@ export function countVarsAfterPush(remoteVars, plan) {
|
|
|
943
1016
|
* local value). Pure and side-effect-free so it's unit-testable without a live
|
|
944
1017
|
* server.
|
|
945
1018
|
*/
|
|
946
|
-
export function diffVars(localVars, remoteVars
|
|
1019
|
+
export function diffVars(localVars, remoteVars,
|
|
1020
|
+
/**
|
|
1021
|
+
* The file `config pull` would write a server value into — a machine-local
|
|
1022
|
+
* environment's override file (#4485). Defaults to `vars.toml`.
|
|
1023
|
+
*/
|
|
1024
|
+
pullTargetFile = "vars.toml") {
|
|
947
1025
|
const rows = [];
|
|
948
1026
|
for (const [key, localValue] of localVars) {
|
|
949
1027
|
if (!remoteVars.has(key)) {
|
|
@@ -954,7 +1032,9 @@ export function diffVars(localVars, remoteVars) {
|
|
|
954
1032
|
type: "var",
|
|
955
1033
|
key,
|
|
956
1034
|
status: "modified",
|
|
957
|
-
hint:
|
|
1035
|
+
hint: pullTargetFile === "vars.toml"
|
|
1036
|
+
? "run `config pull` to rewrite vars.toml to match the server value"
|
|
1037
|
+
: `run \`config pull\` to write the server value into ${pullTargetFile}`,
|
|
958
1038
|
});
|
|
959
1039
|
}
|
|
960
1040
|
else {
|
|
@@ -2483,7 +2563,7 @@ function localConfigWithServerIdentity(label, local, server, file) {
|
|
|
2483
2563
|
* that being a visible change to this signature.
|
|
2484
2564
|
*/
|
|
2485
2565
|
export async function runConfigPushPreflight(input) {
|
|
2486
|
-
const { configDir, resolvedAppId, pushSelection, syncState, force, isCrossAppPush, localEnvironment = false, client, } = input;
|
|
2566
|
+
const { configDir, resolvedAppId, pushSelection, syncState, force, isCrossAppPush, localEnvironment = false, client, varsOverridePath = null, } = input;
|
|
2487
2567
|
/**
|
|
2488
2568
|
* Narrow a type's file list to the `--only` selection — the same one-line
|
|
2489
2569
|
* closure the action holds, rebuilt from the selection it passed in rather
|
|
@@ -2609,6 +2689,9 @@ export async function runConfigPushPreflight(input) {
|
|
|
2609
2689
|
fileName: parsedApp?.app?.name,
|
|
2610
2690
|
}),
|
|
2611
2691
|
nameOptional: localEnvironment,
|
|
2692
|
+
// #4564 — the protection level follows the name on a machine-local
|
|
2693
|
+
// environment: out of the body, so out of what is validated.
|
|
2694
|
+
excludeProtection: localEnvironment,
|
|
2612
2695
|
});
|
|
2613
2696
|
for (const e of appErrors) {
|
|
2614
2697
|
preflightValidationErrors.push(` app.toml: ${e}`);
|
|
@@ -2627,7 +2710,9 @@ export async function runConfigPushPreflight(input) {
|
|
|
2627
2710
|
const preflightVarsTomlPath = join(configDir, "vars.toml");
|
|
2628
2711
|
const varsSelected = selectionIncludesAnyVar(pushSelection);
|
|
2629
2712
|
let preflightParsedVars = null;
|
|
2630
|
-
|
|
2713
|
+
let varsOverrideKeys = null;
|
|
2714
|
+
const overrideExists = !!varsOverridePath && existsSync(varsOverridePath);
|
|
2715
|
+
if (varsSelected && existsSync(preflightVarsTomlPath) && !overrideExists) {
|
|
2631
2716
|
try {
|
|
2632
2717
|
const parsed = parseTomlFile(preflightVarsTomlPath);
|
|
2633
2718
|
// #2645 — under `--only var/<key>` only the selected entries are
|
|
@@ -2645,6 +2730,49 @@ export async function runConfigPushPreflight(input) {
|
|
|
2645
2730
|
preflightValidationErrors.push(` vars.toml: ${err?.message || String(err)}`);
|
|
2646
2731
|
}
|
|
2647
2732
|
}
|
|
2733
|
+
else if (varsSelected && overrideExists) {
|
|
2734
|
+
// #4485 — a machine-local environment's own values, layered over
|
|
2735
|
+
// vars.toml. Each file is validated under its own name, and the count
|
|
2736
|
+
// cap is measured on the layered set: that is what the push sends.
|
|
2737
|
+
const overrideFile = basename(varsOverridePath);
|
|
2738
|
+
const onlyKeys = pushSelection ? pushSelection.varKeys : undefined;
|
|
2739
|
+
const tables = [];
|
|
2740
|
+
let parsedAll = true;
|
|
2741
|
+
for (const [path, fileName] of [
|
|
2742
|
+
[preflightVarsTomlPath, "vars.toml"],
|
|
2743
|
+
[varsOverridePath, overrideFile],
|
|
2744
|
+
]) {
|
|
2745
|
+
if (!existsSync(path)) {
|
|
2746
|
+
tables.push(null);
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
try {
|
|
2750
|
+
const parsed = parseTomlFile(path);
|
|
2751
|
+
for (const e of validateVarsFile(parsed, { onlyKeys, fileName, skipCountCap: true })) {
|
|
2752
|
+
preflightValidationErrors.push(` ${e}`);
|
|
2753
|
+
}
|
|
2754
|
+
tables.push(parsed);
|
|
2755
|
+
}
|
|
2756
|
+
catch (err) {
|
|
2757
|
+
parsedAll = false;
|
|
2758
|
+
preflightValidationErrors.push(` ${fileName}: ${err?.message || String(err)}`);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
if (parsedAll) {
|
|
2762
|
+
const [base, override] = tables;
|
|
2763
|
+
const layered = layerVars(base, override);
|
|
2764
|
+
const count = Object.keys(layered).length;
|
|
2765
|
+
if (!onlyKeys && count > MAX_VARS_PER_APP) {
|
|
2766
|
+
preflightValidationErrors.push(` vars.toml and ${overrideFile} layer to ${count} vars, exceeding the maximum of ${MAX_VARS_PER_APP} vars per app. Remove ${count - MAX_VARS_PER_APP} entr${count - MAX_VARS_PER_APP === 1 ? "y" : "ies"}.`);
|
|
2767
|
+
}
|
|
2768
|
+
const overrideKeys = Object.keys(override ?? {}).sort();
|
|
2769
|
+
varsOverrideKeys = {
|
|
2770
|
+
overridingKeys: overrideKeys.filter((key) => base !== null && key in base),
|
|
2771
|
+
addedKeys: overrideKeys.filter((key) => base === null || !(key in base)),
|
|
2772
|
+
};
|
|
2773
|
+
preflightParsedVars = layered;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2648
2776
|
// #3182 — a function's capabilities, checked against the author's own
|
|
2649
2777
|
// file before anything is applied. The grammar is purely local (and
|
|
2650
2778
|
// since #3279 it keeps three kinds: `integration:<key>`,
|
|
@@ -2740,6 +2868,20 @@ export async function runConfigPushPreflight(input) {
|
|
|
2740
2868
|
// #3798 — the functions an agent's references are checked against, read
|
|
2741
2869
|
// once and only when a selected prompt is an agent that names one.
|
|
2742
2870
|
let agentFunctions = null;
|
|
2871
|
+
// The app's functions, listed at most once for the #3798 and #4441 checks.
|
|
2872
|
+
// `null` when the listing failed.
|
|
2873
|
+
let liveFunctionRows;
|
|
2874
|
+
const liveFunctionListing = async () => {
|
|
2875
|
+
if (liveFunctionRows === undefined) {
|
|
2876
|
+
try {
|
|
2877
|
+
liveFunctionRows = (await client.listFunctions(resolvedAppId)).items;
|
|
2878
|
+
}
|
|
2879
|
+
catch {
|
|
2880
|
+
liveFunctionRows = null;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return liveFunctionRows;
|
|
2884
|
+
};
|
|
2743
2885
|
if (existsSync(preflightPromptsDir)) {
|
|
2744
2886
|
for (const file of selectFiles("prompt", preflightPromptsDir, readdirSync(preflightPromptsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2745
2887
|
const filePath = join(preflightPromptsDir, file);
|
|
@@ -2757,17 +2899,10 @@ export async function runConfigPushPreflight(input) {
|
|
|
2757
2899
|
if (promptKindOf(tomlData?.prompt) === "agent" &&
|
|
2758
2900
|
agentDeclarationFunctionKeys(tomlData?.prompt?.agent).length > 0) {
|
|
2759
2901
|
if (!agentFunctions) {
|
|
2760
|
-
let live = null;
|
|
2761
|
-
try {
|
|
2762
|
-
live = (await client.listFunctions(resolvedAppId)).items;
|
|
2763
|
-
}
|
|
2764
|
-
catch {
|
|
2765
|
-
live = null;
|
|
2766
|
-
}
|
|
2767
2902
|
agentFunctions = agentFunctionFactsForPush({
|
|
2768
2903
|
configDir,
|
|
2769
2904
|
pushSelection,
|
|
2770
|
-
live,
|
|
2905
|
+
live: await liveFunctionListing(),
|
|
2771
2906
|
});
|
|
2772
2907
|
}
|
|
2773
2908
|
for (const message of promptAgentFunctionErrors(filePath, tomlData, agentFunctions)) {
|
|
@@ -2776,6 +2911,88 @@ export async function runConfigPushPreflight(input) {
|
|
|
2776
2911
|
}
|
|
2777
2912
|
}
|
|
2778
2913
|
}
|
|
2914
|
+
// #4441 — a function file this push CHANGES, checked against the live
|
|
2915
|
+
// agents that use it: refused here when an agent the push does not select
|
|
2916
|
+
// cannot take the change, and the agent files that must release a reference
|
|
2917
|
+
// first are moved ahead of the functions leg. Lists the app's prompts only
|
|
2918
|
+
// when a selected function file changes a live function.
|
|
2919
|
+
let agentFilesFirst = [];
|
|
2920
|
+
const preflightFunctionFilesDir = join(configDir, "functions");
|
|
2921
|
+
const selectedFunctionFiles = [];
|
|
2922
|
+
if (existsSync(preflightFunctionFilesDir)) {
|
|
2923
|
+
for (const file of selectFiles("function", preflightFunctionFilesDir, readdirSync(preflightFunctionFilesDir).filter(isFunctionDeclarationFile))) {
|
|
2924
|
+
let table;
|
|
2925
|
+
try {
|
|
2926
|
+
table = parseTomlFile(join(preflightFunctionFilesDir, file))?.function ?? {};
|
|
2927
|
+
}
|
|
2928
|
+
catch {
|
|
2929
|
+
// Reported by the function preflight with its own message.
|
|
2930
|
+
continue;
|
|
2931
|
+
}
|
|
2932
|
+
selectedFunctionFiles.push({ file, key: table.key || basename(file, ".toml") });
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
if (selectedFunctionFiles.length > 0) {
|
|
2936
|
+
const liveRows = await liveFunctionListing();
|
|
2937
|
+
const live = liveAgentFunctionFacts(liveRows);
|
|
2938
|
+
const changed = functionChangePreflightKeys(selectedFunctionFiles, live);
|
|
2939
|
+
if (liveRows === null) {
|
|
2940
|
+
warn("The app's functions could not be listed, so the function files this " +
|
|
2941
|
+
"push changes were not checked against the agents that use them. The " +
|
|
2942
|
+
"server still refuses a change an agent cannot take.");
|
|
2943
|
+
}
|
|
2944
|
+
else if (changed.length > 0) {
|
|
2945
|
+
let prompts = null;
|
|
2946
|
+
const configsOf = new Map();
|
|
2947
|
+
try {
|
|
2948
|
+
prompts = await fetchAll((p) => client.listPrompts(resolvedAppId, p));
|
|
2949
|
+
for (const agent of functionChangePreflightAgents(prompts, changed.map((entry) => entry.key))) {
|
|
2950
|
+
const { items } = await client.listPromptConfigs(resolvedAppId, agent.promptId);
|
|
2951
|
+
configsOf.set(agent.promptId, items.map((config) => ({
|
|
2952
|
+
configName: String(config?.configName ?? ""),
|
|
2953
|
+
provider: String(config?.provider ?? ""),
|
|
2954
|
+
status: String(config?.status ?? "active"),
|
|
2955
|
+
})));
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
catch {
|
|
2959
|
+
prompts = null;
|
|
2960
|
+
}
|
|
2961
|
+
const selectedAgentFiles = [];
|
|
2962
|
+
if (existsSync(preflightPromptsDir)) {
|
|
2963
|
+
for (const file of selectFiles("prompt", preflightPromptsDir, readdirSync(preflightPromptsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2964
|
+
let tomlData;
|
|
2965
|
+
try {
|
|
2966
|
+
tomlData = parseTomlFile(join(preflightPromptsDir, file));
|
|
2967
|
+
}
|
|
2968
|
+
catch {
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
if (promptKindOf(tomlData?.prompt) !== "agent")
|
|
2972
|
+
continue;
|
|
2973
|
+
selectedAgentFiles.push({
|
|
2974
|
+
file,
|
|
2975
|
+
promptKey: tomlData?.prompt?.key || basename(file, ".toml"),
|
|
2976
|
+
tomlData,
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const checked = functionChangePreflight({
|
|
2981
|
+
selectedFunctionFiles,
|
|
2982
|
+
live,
|
|
2983
|
+
pushed: agentFunctionFactsForPush({ configDir, pushSelection, live: liveRows }).facts,
|
|
2984
|
+
prompts,
|
|
2985
|
+
configsOf,
|
|
2986
|
+
selectedAgentFiles,
|
|
2987
|
+
});
|
|
2988
|
+
for (const message of checked.warnings)
|
|
2989
|
+
warn(message);
|
|
2990
|
+
for (const message of checked.errors) {
|
|
2991
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2992
|
+
}
|
|
2993
|
+
agentFilesFirst = checked.agentFilesFirst;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2779
2996
|
// #3798, #4262 — a prompt's sidecar against its prompt's kind: an agent's
|
|
2780
2997
|
// case is checked by the agent test case's shape rule, and `[test.agent]`
|
|
2781
2998
|
// on a chat or decisions prompt's case is refused, before any file is
|
|
@@ -2805,7 +3022,7 @@ export async function runConfigPushPreflight(input) {
|
|
|
2805
3022
|
throw new Error(`Aborting push: ${preflightValidationErrors.length} TOML validation error(s) — no changes were applied.\n` +
|
|
2806
3023
|
preflightValidationErrors.join("\n"));
|
|
2807
3024
|
}
|
|
2808
|
-
return { parsedVars: preflightParsedVars };
|
|
3025
|
+
return { parsedVars: preflightParsedVars, agentFilesFirst, varsOverrideKeys };
|
|
2809
3026
|
}
|
|
2810
3027
|
/**
|
|
2811
3028
|
* Every per-entity config file whose TOML `config push` would refuse (#2880
|
|
@@ -3037,21 +3254,9 @@ function declaresSchema(value) {
|
|
|
3037
3254
|
* availability on every call).
|
|
3038
3255
|
*/
|
|
3039
3256
|
export function agentFunctionFactsForPush(input) {
|
|
3040
|
-
const facts = {
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
if (!key || row?.status === "archived")
|
|
3044
|
-
continue;
|
|
3045
|
-
facts[key] = {
|
|
3046
|
-
hasInputSchema: declaresSchema(row.inputSchema),
|
|
3047
|
-
hasOutputSchema: declaresSchema(row.outputSchema),
|
|
3048
|
-
// #3799 — the schemas themselves, as stored (a JSON string from the API,
|
|
3049
|
-
// an object from a tree file), so a server tool's two schemas can be
|
|
3050
|
-
// translated for each of the agent's providers.
|
|
3051
|
-
inputSchema: row.inputSchema,
|
|
3052
|
-
outputSchema: row.outputSchema,
|
|
3053
|
-
};
|
|
3054
|
-
}
|
|
3257
|
+
const facts = {
|
|
3258
|
+
...liveAgentFunctionFacts(input.live),
|
|
3259
|
+
};
|
|
3055
3260
|
const functionsDir = join(input.configDir, "functions");
|
|
3056
3261
|
if (existsSync(functionsDir)) {
|
|
3057
3262
|
for (const file of filterFilesBySelection(input.pushSelection, "function", functionsDir, readdirSync(functionsDir).filter(isFunctionDeclarationFile))) {
|
|
@@ -3076,6 +3281,196 @@ export function agentFunctionFactsForPush(input) {
|
|
|
3076
3281
|
}
|
|
3077
3282
|
return { facts, liveListed: input.live !== null };
|
|
3078
3283
|
}
|
|
3284
|
+
/**
|
|
3285
|
+
* The functions the app holds now, as the agent rules read them — #3798. A
|
|
3286
|
+
* live row that is archived is a tombstone, not a function, so it is absent.
|
|
3287
|
+
*/
|
|
3288
|
+
export function liveAgentFunctionFacts(live) {
|
|
3289
|
+
const facts = {};
|
|
3290
|
+
for (const row of live ?? []) {
|
|
3291
|
+
const key = String(row?.functionKey ?? "");
|
|
3292
|
+
if (!key || row?.status === "archived")
|
|
3293
|
+
continue;
|
|
3294
|
+
facts[key] = {
|
|
3295
|
+
hasInputSchema: declaresSchema(row.inputSchema),
|
|
3296
|
+
hasOutputSchema: declaresSchema(row.outputSchema),
|
|
3297
|
+
// #3799 — the schemas themselves, as stored (a JSON string from the API,
|
|
3298
|
+
// an object from a tree file), so a server tool's two schemas can be
|
|
3299
|
+
// translated for each of the agent's providers.
|
|
3300
|
+
inputSchema: row.inputSchema,
|
|
3301
|
+
outputSchema: row.outputSchema,
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
return facts;
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* The selected function files that CHANGE a live function — #4441. A file
|
|
3308
|
+
* whose function is not live creates it, which no agent can depend on yet.
|
|
3309
|
+
* The push lists the app's prompts only when this is non-empty.
|
|
3310
|
+
*/
|
|
3311
|
+
export function functionChangePreflightKeys(selectedFunctionFiles, live) {
|
|
3312
|
+
return selectedFunctionFiles.filter((entry) => Object.prototype.hasOwnProperty.call(live, entry.key) && live[entry.key] != null);
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* The live, non-archived agent prompts that name at least one of `keys`.
|
|
3316
|
+
* Whose configs the push reads for `functionChangePreflight`.
|
|
3317
|
+
*/
|
|
3318
|
+
export function functionChangePreflightAgents(prompts, keys) {
|
|
3319
|
+
return prompts.filter((prompt) => promptKindOf(prompt) === "agent" &&
|
|
3320
|
+
prompt.status !== "archived" &&
|
|
3321
|
+
agentDeclarationFunctionKeys(prompt.agent).some((key) => keys.includes(key)));
|
|
3322
|
+
}
|
|
3323
|
+
/** `tool "lookup"` for a tool's name, the path itself for a hook. */
|
|
3324
|
+
function functionUseLabels(uses) {
|
|
3325
|
+
return uses.map((use) => (use.includes(".") ? use : `tool "${use}"`)).join(", ");
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Would a function change this push makes break a live agent? — #4441.
|
|
3329
|
+
*
|
|
3330
|
+
* The same rule the function routes run (`agentFunctionChangeRefusals`), so a
|
|
3331
|
+
* change the preflight lets through is one the server accepts, run for every
|
|
3332
|
+
* selected file that changes a live function against every live non-archived
|
|
3333
|
+
* agent that names it, with every config of that agent:
|
|
3334
|
+
*
|
|
3335
|
+
* - an agent whose prompt file this push does NOT select would be broken by
|
|
3336
|
+
* the change: a preflight error naming the function file, the agent and its
|
|
3337
|
+
* uses, and the remedy (change the agent in the same push, or retire it);
|
|
3338
|
+
* - an agent whose file IS selected keeps the existing order (applied after
|
|
3339
|
+
* the functions leg) unless its LIVE declaration would be broken by the
|
|
3340
|
+
* change. Then the file must go first to release the reference, which it
|
|
3341
|
+
* can only do when it is valid against the functions the app holds NOW —
|
|
3342
|
+
* its references, the providers of its non-archived configs, and the
|
|
3343
|
+
* providers of the configs the server still holds at its prompt PATCH. When it
|
|
3344
|
+
* is, it joins `agentFilesFirst`; when it is not, the push cannot be
|
|
3345
|
+
* ordered in one run and the error names the sequence that can.
|
|
3346
|
+
*
|
|
3347
|
+
* `prompts: null` (the listing failed) checks nothing and says so once; the
|
|
3348
|
+
* server's refusal remains.
|
|
3349
|
+
*/
|
|
3350
|
+
export function functionChangePreflight(input) {
|
|
3351
|
+
const result = {
|
|
3352
|
+
errors: [],
|
|
3353
|
+
warnings: [],
|
|
3354
|
+
agentFilesFirst: [],
|
|
3355
|
+
};
|
|
3356
|
+
const changed = functionChangePreflightKeys(input.selectedFunctionFiles, input.live);
|
|
3357
|
+
if (changed.length === 0)
|
|
3358
|
+
return result;
|
|
3359
|
+
if (input.prompts === null) {
|
|
3360
|
+
result.warnings.push("The app's prompts could not be listed, so the function files this push " +
|
|
3361
|
+
"changes were not checked against the agents that use them. The server " +
|
|
3362
|
+
"still refuses a change an agent cannot take.");
|
|
3363
|
+
return result;
|
|
3364
|
+
}
|
|
3365
|
+
const agents = functionChangePreflightAgents(input.prompts, changed.map((entry) => entry.key));
|
|
3366
|
+
for (const agent of agents) {
|
|
3367
|
+
const configs = input.configsOf.get(agent.promptId) ?? [];
|
|
3368
|
+
const named = agentDeclarationFunctionKeys(agent.agent);
|
|
3369
|
+
// Per changed function, so each refusal is attributed to its file.
|
|
3370
|
+
const broken = [];
|
|
3371
|
+
for (const entry of changed) {
|
|
3372
|
+
if (!named.includes(entry.key))
|
|
3373
|
+
continue;
|
|
3374
|
+
const refusals = agentFunctionChangeRefusals({
|
|
3375
|
+
declaration: agent.agent,
|
|
3376
|
+
configs,
|
|
3377
|
+
before: input.live,
|
|
3378
|
+
after: { ...input.live, [entry.key]: input.pushed[entry.key] ?? null },
|
|
3379
|
+
});
|
|
3380
|
+
if (refusals.length > 0)
|
|
3381
|
+
broken.push({ entry, refusals });
|
|
3382
|
+
}
|
|
3383
|
+
if (broken.length === 0)
|
|
3384
|
+
continue;
|
|
3385
|
+
const agentFile = input.selectedAgentFiles.find((candidate) => candidate.promptKey === agent.promptKey);
|
|
3386
|
+
if (!agentFile) {
|
|
3387
|
+
for (const { entry, refusals } of broken) {
|
|
3388
|
+
result.errors.push(`functions/${entry.file}: agent "${agent.promptKey}" ` +
|
|
3389
|
+
`(prompts/${agent.promptKey}.toml, not in this push) uses function ` +
|
|
3390
|
+
`"${entry.key}" as ${functionUseLabels(agentFunctionUses(agent.agent, entry.key))}, ` +
|
|
3391
|
+
`and this change would break it: ` +
|
|
3392
|
+
`${refusals.map((refusal) => refusal.message).join("; ")}. Change the ` +
|
|
3393
|
+
"agent in this same push so it no longer needs what the change " +
|
|
3394
|
+
"removes, or retire the agent.");
|
|
3395
|
+
}
|
|
3396
|
+
continue;
|
|
3397
|
+
}
|
|
3398
|
+
// The file must go first. Can it, against the functions the app holds now?
|
|
3399
|
+
// Its prompt PATCH is checked against the configs the server still holds
|
|
3400
|
+
// at that moment (CSO-4441-001): every non-archived one except those the
|
|
3401
|
+
// file retires, which the apply stages ahead of the PATCH. The file's own
|
|
3402
|
+
// config writes follow the PATCH and are checked against its entries.
|
|
3403
|
+
const fileAgent = agentFile.tomlData?.prompt?.agent;
|
|
3404
|
+
const blockers = promptAgentFunctionRefusalsAgainst(agentFile.tomlData, input.live, heldAtPromptPatch(agentFile.tomlData, configs));
|
|
3405
|
+
const blocking = broken.map(({ entry }) => `functions/${entry.file}`).join(", ");
|
|
3406
|
+
if (blockers.length === 0) {
|
|
3407
|
+
result.agentFilesFirst.push(agentFile.file);
|
|
3408
|
+
continue;
|
|
3409
|
+
}
|
|
3410
|
+
const created = agentDeclarationFunctionKeys(fileAgent).filter((key) => input.live[key] == null && input.pushed[key] != null);
|
|
3411
|
+
const lead = `prompts/${agentFile.file}: agent "${agent.promptKey}" must be applied ` +
|
|
3412
|
+
`before ${blocking}, because its live declaration cannot take that ` +
|
|
3413
|
+
"change";
|
|
3414
|
+
if (created.length > 0) {
|
|
3415
|
+
result.errors.push(`${lead}, but it names ${created.map((key) => `function "${key}"`).join(", ")}, ` +
|
|
3416
|
+
"which this push creates. Push the new function first, then rerun " +
|
|
3417
|
+
`this push: primitive config push --only ` +
|
|
3418
|
+
`${created.map((key) => `function/${key}`).join(",")}`);
|
|
3419
|
+
}
|
|
3420
|
+
else {
|
|
3421
|
+
result.errors.push(`${lead}, but it cannot be applied against the functions and configs ` +
|
|
3422
|
+
`the app holds now: ${blockers.map((refusal) => refusal.message).join("; ")}. ` +
|
|
3423
|
+
"Split the change: push the agent's change on its own first, without " +
|
|
3424
|
+
"what the live functions and configs cannot satisfy, then rerun this push.");
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
return result;
|
|
3428
|
+
}
|
|
3429
|
+
/**
|
|
3430
|
+
* The function refusals an agent prompt file owes against `functions`: a
|
|
3431
|
+
* missing function or tool schema, and a server-tool schema one of the file's
|
|
3432
|
+
* non-archived configs cannot carry. The file-decidable refusals are left to
|
|
3433
|
+
* the collector that already reports them.
|
|
3434
|
+
*/
|
|
3435
|
+
function promptAgentFunctionRefusalsAgainst(tomlData, functions, heldConfigs = []) {
|
|
3436
|
+
const agent = tomlData?.prompt?.agent;
|
|
3437
|
+
const resolved = resolveAgentDeclaration({ kind: "agent", agent, functions });
|
|
3438
|
+
const refusals = resolved.ok === false
|
|
3439
|
+
? resolved.refusals.filter((refusal) => refusal.code === "AGENT_TOOL_FUNCTION_MISSING" ||
|
|
3440
|
+
refusal.code === "AGENT_TOOL_FUNCTION_SCHEMA_MISSING")
|
|
3441
|
+
: [];
|
|
3442
|
+
const seen = new Set();
|
|
3443
|
+
for (const configs of [
|
|
3444
|
+
promptFileConfigFacts(Array.isArray(tomlData?.configs) ? tomlData.configs : []),
|
|
3445
|
+
[...heldConfigs],
|
|
3446
|
+
]) {
|
|
3447
|
+
const fileDecidable = new Set(agentSchemaTranslationRefusals({ declaration: agent, configs }).map((refusal) => refusal.message));
|
|
3448
|
+
for (const refusal of agentSchemaTranslationRefusals({
|
|
3449
|
+
declaration: agent,
|
|
3450
|
+
functions,
|
|
3451
|
+
configs,
|
|
3452
|
+
})) {
|
|
3453
|
+
if (fileDecidable.has(refusal.message) || seen.has(refusal.message))
|
|
3454
|
+
continue;
|
|
3455
|
+
seen.add(refusal.message);
|
|
3456
|
+
refusals.push(refusal);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
return refusals;
|
|
3460
|
+
}
|
|
3461
|
+
/**
|
|
3462
|
+
* The configs the server holds for an agent when a push's prompt PATCH
|
|
3463
|
+
* reaches it — #4441 (CSO-4441-001): every non-archived one but those the
|
|
3464
|
+
* file archives, which the apply stages ahead of the PATCH. Each keeps the
|
|
3465
|
+
* provider it has NOW; the file's change to it is sent after the PATCH.
|
|
3466
|
+
*/
|
|
3467
|
+
function heldAtPromptPatch(tomlData, configs) {
|
|
3468
|
+
const retired = new Set(promptFileConfigFacts(Array.isArray(tomlData?.configs) ? tomlData.configs : [])
|
|
3469
|
+
.filter((entry) => entry.status.trim().toLowerCase() === "archived")
|
|
3470
|
+
.map((entry) => entry.configName));
|
|
3471
|
+
return configs.filter((config) => String(config.status ?? "active").trim().toLowerCase() !== "archived" &&
|
|
3472
|
+
!retired.has(config.configName));
|
|
3473
|
+
}
|
|
3079
3474
|
/**
|
|
3080
3475
|
* The function half of an agent's declaration — #3798: every tool, turn
|
|
3081
3476
|
* context and history function that the functions this push believes
|
|
@@ -3130,9 +3525,10 @@ export function promptAgentFunctionErrors(filePath, tomlData, known) {
|
|
|
3130
3525
|
* server functions that must exist when it is created, and the push applies
|
|
3131
3526
|
* prompts before functions, so agent files go AFTER the server-functions leg;
|
|
3132
3527
|
* every other file keeps its place. A file that does not parse stays first, so
|
|
3133
|
-
* the leg reports it where it always has.
|
|
3528
|
+
* the leg reports it where it always has. #4441 — the agent files in
|
|
3529
|
+
* `agentFilesFirst` (`functionChangePreflight`) go before the leg instead.
|
|
3134
3530
|
*/
|
|
3135
|
-
export function partitionPromptFilesForPush(promptsDir, files) {
|
|
3531
|
+
export function partitionPromptFilesForPush(promptsDir, files, agentFilesFirst = []) {
|
|
3136
3532
|
const beforeFunctions = [];
|
|
3137
3533
|
const afterFunctions = [];
|
|
3138
3534
|
for (const file of files) {
|
|
@@ -3144,7 +3540,11 @@ export function partitionPromptFilesForPush(promptsDir, files) {
|
|
|
3144
3540
|
beforeFunctions.push(file);
|
|
3145
3541
|
continue;
|
|
3146
3542
|
}
|
|
3147
|
-
|
|
3543
|
+
// #4441 — an agent file the preflight found must release a function
|
|
3544
|
+
// reference before the functions leg changes that function.
|
|
3545
|
+
(kind === "agent" && !agentFilesFirst.includes(file)
|
|
3546
|
+
? afterFunctions
|
|
3547
|
+
: beforeFunctions).push(file);
|
|
3148
3548
|
}
|
|
3149
3549
|
return { beforeFunctions, afterFunctions };
|
|
3150
3550
|
}
|
|
@@ -6343,8 +6743,8 @@ export async function listAllTestCases(client, appId, blockType, blockId) {
|
|
|
6343
6743
|
try {
|
|
6344
6744
|
page = await client.listTestCases(appId, blockType, blockId, cursor ? { cursor } : {});
|
|
6345
6745
|
}
|
|
6346
|
-
catch {
|
|
6347
|
-
return { ok: false };
|
|
6746
|
+
catch (error) {
|
|
6747
|
+
return { ok: false, error };
|
|
6348
6748
|
}
|
|
6349
6749
|
items.push(...(page.items || []));
|
|
6350
6750
|
const next = page.nextCursor;
|
|
@@ -6821,6 +7221,7 @@ export async function pushTestCasesForBlock(params) {
|
|
|
6821
7221
|
const testCaseExtra = { lookupMaps: params.lookupMaps };
|
|
6822
7222
|
const listed = await listAllTestCases(client, appId, blockType, blockId);
|
|
6823
7223
|
const liveOk = listed.ok;
|
|
7224
|
+
const liveReadTransient = "error" in listed && isTransientRequestFailure(listed.error);
|
|
6824
7225
|
if (!liveOk) {
|
|
6825
7226
|
warn(` ${blockKey} test cases: live state unavailable — falling back to last-sync content hashes`);
|
|
6826
7227
|
}
|
|
@@ -7082,6 +7483,7 @@ export async function pushTestCasesForBlock(params) {
|
|
|
7082
7483
|
type: "test-case",
|
|
7083
7484
|
key: `${blockKey}/${slug}`,
|
|
7084
7485
|
message,
|
|
7486
|
+
transient: isTransientRequestFailure(err),
|
|
7085
7487
|
});
|
|
7086
7488
|
}
|
|
7087
7489
|
continue;
|
|
@@ -7129,7 +7531,7 @@ export async function pushTestCasesForBlock(params) {
|
|
|
7129
7531
|
continue;
|
|
7130
7532
|
}
|
|
7131
7533
|
if (degraded.action === "decline") {
|
|
7132
|
-
params.declined?.("test-case", `${blockKey}/${slug}`, { action: "live-unavailable" }, existingEntry?.modifiedAt);
|
|
7534
|
+
params.declined?.("test-case", `${blockKey}/${slug}`, { action: "live-unavailable", transient: liveReadTransient }, existingEntry?.modifiedAt);
|
|
7133
7535
|
continue;
|
|
7134
7536
|
}
|
|
7135
7537
|
}
|
|
@@ -7192,7 +7594,12 @@ export async function pushTestCasesForBlock(params) {
|
|
|
7192
7594
|
catch (err) {
|
|
7193
7595
|
const message = err?.message || String(err);
|
|
7194
7596
|
warn(` Failed to update test case ${blockKey}/${slug}: ${message}`);
|
|
7195
|
-
failures.push({
|
|
7597
|
+
failures.push({
|
|
7598
|
+
type: "test-case",
|
|
7599
|
+
key: `${blockKey}/${slug}`,
|
|
7600
|
+
message,
|
|
7601
|
+
transient: isTransientRequestFailure(err),
|
|
7602
|
+
});
|
|
7196
7603
|
continue;
|
|
7197
7604
|
}
|
|
7198
7605
|
}
|
|
@@ -7266,7 +7673,12 @@ export async function pushTestCasesForBlock(params) {
|
|
|
7266
7673
|
? `${keyError} — rename ${file} to a path-safe file name`
|
|
7267
7674
|
: serverMessage;
|
|
7268
7675
|
warn(` Failed to create test case ${blockKey}/${slug}: ${message}`);
|
|
7269
|
-
failures.push({
|
|
7676
|
+
failures.push({
|
|
7677
|
+
type: "test-case",
|
|
7678
|
+
key: `${blockKey}/${slug}`,
|
|
7679
|
+
message,
|
|
7680
|
+
transient: isTransientRequestFailure(err),
|
|
7681
|
+
});
|
|
7270
7682
|
continue;
|
|
7271
7683
|
}
|
|
7272
7684
|
}
|
|
@@ -7310,6 +7722,7 @@ export async function pushTestCasesForBlock(params) {
|
|
|
7310
7722
|
type: "attachment",
|
|
7311
7723
|
key: `${blockKey}/${slug}/${filename}`,
|
|
7312
7724
|
message,
|
|
7725
|
+
transient: isTransientRequestFailure(err),
|
|
7313
7726
|
});
|
|
7314
7727
|
}
|
|
7315
7728
|
}
|
|
@@ -7881,7 +8294,7 @@ export async function applyTestCasePrune(params) {
|
|
|
7881
8294
|
catch (err) {
|
|
7882
8295
|
const message = err?.message || String(err);
|
|
7883
8296
|
warn(` Failed to delete attachment ${key}: ${message}`);
|
|
7884
|
-
params.failures.push({ type: "attachment", key, message });
|
|
8297
|
+
params.failures.push({ type: "attachment", key, message, transient: isTransientRequestFailure(err) });
|
|
7885
8298
|
}
|
|
7886
8299
|
}
|
|
7887
8300
|
for (const candidate of params.candidates) {
|
|
@@ -7916,7 +8329,7 @@ export async function applyTestCasePrune(params) {
|
|
|
7916
8329
|
catch (err) {
|
|
7917
8330
|
const message = err?.message || String(err);
|
|
7918
8331
|
warn(` Failed to delete test case ${key}: ${message}`);
|
|
7919
|
-
params.failures.push({ type: "test-case", key, message });
|
|
8332
|
+
params.failures.push({ type: "test-case", key, message, transient: isTransientRequestFailure(err) });
|
|
7920
8333
|
}
|
|
7921
8334
|
}
|
|
7922
8335
|
return { deleted, deletedAttachments };
|
|
@@ -8020,6 +8433,8 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8020
8433
|
const resolvedAppId = getCurrentAppId();
|
|
8021
8434
|
const configDir = resolveSyncDir();
|
|
8022
8435
|
const pullsLocalEnvironment = requireCurrentEnvironment().local;
|
|
8436
|
+
// #4485 — null for a committed environment, which never reads one.
|
|
8437
|
+
const pullVarsOverridePath = resolveVarsOverridePath(configDir, requireCurrentEnvironment());
|
|
8023
8438
|
if (!options.json) {
|
|
8024
8439
|
info(`Using per-environment config directory: ${configDir}`);
|
|
8025
8440
|
}
|
|
@@ -8312,7 +8727,27 @@ export function registerConfigSyncCommands(sync) {
|
|
|
8312
8727
|
catch (varErr) {
|
|
8313
8728
|
varsOutcome = { ok: false, error: varErr };
|
|
8314
8729
|
}
|
|
8315
|
-
if (
|
|
8730
|
+
if (pullVarsOverridePath) {
|
|
8731
|
+
// #4485 — a machine-local environment never writes vars.toml: the
|
|
8732
|
+
// tree's file is the parent's. Its own values land in its override.
|
|
8733
|
+
if (pullSelection && varsOutcome.ok) {
|
|
8734
|
+
for (const v of varsOutcome.vars) {
|
|
8735
|
+
if (pullSelection.varKeys.has(v.key))
|
|
8736
|
+
matchedSelectors.add(`var/${v.key}`);
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8739
|
+
pullVarsIntoOverride({
|
|
8740
|
+
outcome: varsOutcome,
|
|
8741
|
+
overridePath: pullVarsOverridePath,
|
|
8742
|
+
configDir,
|
|
8743
|
+
env: requireCurrentEnvironment(),
|
|
8744
|
+
selectedKeys: pullSelection ? pullSelection.varKeys : null,
|
|
8745
|
+
priorVars: priorEntities.vars,
|
|
8746
|
+
varEntities,
|
|
8747
|
+
tally: pullWrites,
|
|
8748
|
+
});
|
|
8749
|
+
}
|
|
8750
|
+
else if (pullSelection && varsOutcome.ok) {
|
|
8316
8751
|
// Scoped: merge the named keys into the file that is already there
|
|
8317
8752
|
// rather than re-emitting the whole var surface.
|
|
8318
8753
|
for (const v of varsOutcome.vars) {
|
|
@@ -9239,6 +9674,7 @@ export function registerConfigSyncCommands(sync) {
|
|
|
9239
9674
|
// tree's name and whose baseline never includes one.
|
|
9240
9675
|
semanticHash: hashAppSettingsForDiff(settings, {
|
|
9241
9676
|
excludeName: pullsLocalEnvironment,
|
|
9677
|
+
excludeProtection: pullsLocalEnvironment,
|
|
9242
9678
|
}),
|
|
9243
9679
|
}
|
|
9244
9680
|
: pullSelection
|
|
@@ -9403,6 +9839,9 @@ What a push guarantees (two stages):
|
|
|
9403
9839
|
// tree, so `[app].name` in it is that environment's name: never sent,
|
|
9404
9840
|
// never compared, never hashed into this environment's baseline.
|
|
9405
9841
|
const localEnvironment = requireCurrentEnvironment().local;
|
|
9842
|
+
// #4485 — a machine-local environment's own var values, layered over
|
|
9843
|
+
// vars.toml. Null for a committed environment, which never reads one.
|
|
9844
|
+
const pushVarsOverridePath = resolveVarsOverridePath(configDir, requireCurrentEnvironment());
|
|
9406
9845
|
// Detect cross-app push: if pushing to a different app, clear entity IDs
|
|
9407
9846
|
// so everything goes through the create path
|
|
9408
9847
|
const isCrossAppPush = syncState && syncState.appId !== resolvedAppId;
|
|
@@ -9432,7 +9871,7 @@ What a push guarantees (two stages):
|
|
|
9432
9871
|
// cross-app reset has to have happened first: it clears the entity ids, so
|
|
9433
9872
|
// there is nothing for prune to retire and a removed key IS a typo again.
|
|
9434
9873
|
if (pushSelection) {
|
|
9435
|
-
const missing = unmatchedSelectors(pushSelection, configDir, trackedSelectorKeys(syncState, { prune: !!options.prune }));
|
|
9874
|
+
const missing = unmatchedSelectors(pushSelection, configDir, trackedSelectorKeys(syncState, { prune: !!options.prune }), pushVarsOverridePath);
|
|
9436
9875
|
if (missing.length > 0) {
|
|
9437
9876
|
error(`No local file declares: ${missing.join(", ")}. Run 'primitive config pull', or 'primitive config create <type> <key>' to create one.`);
|
|
9438
9877
|
process.exit(1);
|
|
@@ -9452,6 +9891,11 @@ What a push guarantees (two stages):
|
|
|
9452
9891
|
try {
|
|
9453
9892
|
const changes = [];
|
|
9454
9893
|
let skippedCount = 0;
|
|
9894
|
+
let functionsWithUnchangedCode = 0;
|
|
9895
|
+
// #4485 — functions whose code axis this push evaluated; any other
|
|
9896
|
+
// function the server holds (left out by `--only`, or not in the tree)
|
|
9897
|
+
// also keeps its version, and with it the vars it has cached.
|
|
9898
|
+
const functionKeysEvaluated = new Set();
|
|
9455
9899
|
const conflicts = [];
|
|
9456
9900
|
// Issue #666: per-class buckets for schema-feature errors. Each entry
|
|
9457
9901
|
// groups failures by the code so the end-of-run report can print
|
|
@@ -9636,7 +10080,13 @@ What a push guarantees (two stages):
|
|
|
9636
10080
|
* never an unconditional write — with the type named so the operator
|
|
9637
10081
|
* knows drift detection was not in force for it this run.
|
|
9638
10082
|
*/
|
|
10083
|
+
/** Whether each live read that failed this run was transient (#4491). */
|
|
10084
|
+
const liveReadFailures = [];
|
|
9639
10085
|
const warnLiveUnavailable = (label, reason) => {
|
|
10086
|
+
// A string reason names a consequence ("rule sets unavailable") of a
|
|
10087
|
+
// read already recorded here.
|
|
10088
|
+
if (typeof reason !== "string")
|
|
10089
|
+
liveReadFailures.push(isTransientRequestFailure(reason));
|
|
9640
10090
|
const message = reason instanceof Error ? reason.message : String(reason ?? "unknown");
|
|
9641
10091
|
warn(` ${label}: live state unavailable (${message}) — falling back to last-sync content hashes`);
|
|
9642
10092
|
};
|
|
@@ -9654,6 +10104,11 @@ What a push guarantees (two stages):
|
|
|
9654
10104
|
applyFailures.push({
|
|
9655
10105
|
type,
|
|
9656
10106
|
key,
|
|
10107
|
+
// A rerun can read what this one could not only when the failed
|
|
10108
|
+
// read was transient; a decliner that does not say which read
|
|
10109
|
+
// failed is judged by every read that failed this run.
|
|
10110
|
+
transient: outcome.transient ??
|
|
10111
|
+
(liveReadFailures.length > 0 && liveReadFailures.every(Boolean)),
|
|
9657
10112
|
message: "live state could not be read and the manifest holds no " +
|
|
9658
10113
|
"last-sync content hash for this file, so push cannot tell " +
|
|
9659
10114
|
"which side moved. Nothing was written. Run `config pull` to " +
|
|
@@ -9831,7 +10286,7 @@ What a push guarantees (two stages):
|
|
|
9831
10286
|
// proved to be reached from it before anything below mutates. Nothing
|
|
9832
10287
|
// about what it checks, the order it checks it, or what it prints
|
|
9833
10288
|
// moved with it.
|
|
9834
|
-
const { parsedVars: preflightParsedVars } = await runConfigPushPreflight({
|
|
10289
|
+
const { parsedVars: preflightParsedVars, agentFilesFirst: preflightAgentFilesFirst, varsOverrideKeys, } = await runConfigPushPreflight({
|
|
9835
10290
|
configDir,
|
|
9836
10291
|
resolvedAppId,
|
|
9837
10292
|
pushSelection,
|
|
@@ -9840,7 +10295,12 @@ What a push guarantees (two stages):
|
|
|
9840
10295
|
isCrossAppPush: !!isCrossAppPush,
|
|
9841
10296
|
localEnvironment,
|
|
9842
10297
|
client,
|
|
10298
|
+
varsOverridePath: pushVarsOverridePath,
|
|
9843
10299
|
});
|
|
10300
|
+
const varsOverrideFile = varsOverrideKeys && pushVarsOverridePath ? basename(pushVarsOverridePath) : null;
|
|
10301
|
+
if (varsOverrideKeys && varsOverrideFile) {
|
|
10302
|
+
info(varsOverrideSummaryLine(varsOverrideFile, varsOverrideKeys));
|
|
10303
|
+
}
|
|
9844
10304
|
// Re-derived, not returned: both are one-line functions of inputs the
|
|
9845
10305
|
// entry point already took, and widening its return type to carry them
|
|
9846
10306
|
// would make the vars apply leg depend on the preflight's internals.
|
|
@@ -9864,7 +10324,7 @@ What a push guarantees (two stages):
|
|
|
9864
10324
|
// is a network call whose failure aborts the push, so making it for a
|
|
9865
10325
|
// singleton nobody selected would let `--only workflow/x` fail over
|
|
9866
10326
|
// vars.toml.
|
|
9867
|
-
if (varsSelected && existsSync(preflightVarsTomlPath)) {
|
|
10327
|
+
if (varsSelected && (existsSync(preflightVarsTomlPath) || varsOverrideFile)) {
|
|
9868
10328
|
try {
|
|
9869
10329
|
remoteVarsSnapshot = await client.listAppConfigVars(resolvedAppId);
|
|
9870
10330
|
}
|
|
@@ -9873,7 +10333,7 @@ What a push guarantees (two stages):
|
|
|
9873
10333
|
throw new Error(`Aborting push: could not fetch remote config vars to check for ` +
|
|
9874
10334
|
`concurrent edits (${err?.message || String(err)}) — no changes ` +
|
|
9875
10335
|
`were applied. Re-run with --force to push without the ` +
|
|
9876
|
-
`concurrency guard
|
|
10336
|
+
`concurrency guard.`, { cause: err });
|
|
9877
10337
|
}
|
|
9878
10338
|
// --force: proceed without a snapshot; the guard is intentionally
|
|
9879
10339
|
// bypassed and every local var is upserted as authoritative. The
|
|
@@ -9922,11 +10382,13 @@ What a push guarantees (two stages):
|
|
|
9922
10382
|
const localKeys = new Set(Object.keys(preflightParsedVars));
|
|
9923
10383
|
const plannedDeletes = new Set(varPlan.deletions);
|
|
9924
10384
|
const remoteOnlyCount = remoteVarsSnapshot.filter((r) => !localKeys.has(r.key) && !plannedDeletes.has(r.key)).length;
|
|
10385
|
+
const pullTarget = varsOverrideFile ?? "vars.toml";
|
|
10386
|
+
const localFiles = varsOverrideFile ? `vars.toml or ${varsOverrideFile}` : "vars.toml";
|
|
9925
10387
|
throw new Error(`Aborting push: it would leave ${finalVarCount} config vars on the server, ` +
|
|
9926
10388
|
`exceeding the maximum of ${MAX_VARS_PER_APP} vars per app ` +
|
|
9927
10389
|
`(server has ${remoteVarsSnapshot.length} vars, ${remoteOnlyCount} of ` +
|
|
9928
|
-
`which are not in
|
|
9929
|
-
`\`config pull\` to bring the remote-only vars into
|
|
10390
|
+
`which are not in ${localFiles}) — no changes were applied. Run ` +
|
|
10391
|
+
`\`config pull\` to bring the remote-only vars into ${pullTarget}, or ` +
|
|
9930
10392
|
`delete unneeded vars, then push again.`);
|
|
9931
10393
|
}
|
|
9932
10394
|
}
|
|
@@ -10038,6 +10500,9 @@ What a push guarantees (two stages):
|
|
|
10038
10500
|
const { settings: appPayload, warnings: appWarnings } = buildAppSettingsPayload(tomlData, {
|
|
10039
10501
|
excludeName: payloadExcludesName,
|
|
10040
10502
|
nameOptional: localEnvironment,
|
|
10503
|
+
// #4564 — on a machine-local environment the level is the
|
|
10504
|
+
// shared tree's, like the name, and never this child's.
|
|
10505
|
+
excludeProtection: localEnvironment,
|
|
10041
10506
|
});
|
|
10042
10507
|
for (const w of appWarnings) {
|
|
10043
10508
|
warn(` ${w}`);
|
|
@@ -10067,6 +10532,7 @@ What a push guarantees (two stages):
|
|
|
10067
10532
|
excludeName: localEnvironment ||
|
|
10068
10533
|
options.recordExclusion !== undefined ||
|
|
10069
10534
|
(options.keepExclusion && compareExcludesName),
|
|
10535
|
+
excludeProtection: localEnvironment,
|
|
10070
10536
|
}),
|
|
10071
10537
|
...(options.recordExclusion !== undefined
|
|
10072
10538
|
? { excludedName: options.recordExclusion }
|
|
@@ -10077,8 +10543,17 @@ What a push guarantees (two stages):
|
|
|
10077
10543
|
};
|
|
10078
10544
|
let applyAppSettings = true;
|
|
10079
10545
|
if (appLiveOk) {
|
|
10080
|
-
const compareOptions = {
|
|
10081
|
-
|
|
10546
|
+
const compareOptions = {
|
|
10547
|
+
excludeName: compareExcludesName,
|
|
10548
|
+
excludeProtection: localEnvironment,
|
|
10549
|
+
};
|
|
10550
|
+
// The file's deprecated `protected` key reads against the live
|
|
10551
|
+
// level, as the server's write will (DSO-4564-001), so a change
|
|
10552
|
+
// made only through it is not skipped as unchanged.
|
|
10553
|
+
const localHash = hashAppSettingsForDiff(appPayload, {
|
|
10554
|
+
...compareOptions,
|
|
10555
|
+
legacyProtectedAgainst: settingsProtectionLevel(liveSettings ?? {}),
|
|
10556
|
+
});
|
|
10082
10557
|
const remoteHash = hashAppSettingsForDiff(liveSettings ?? {}, compareOptions);
|
|
10083
10558
|
if (options.force) {
|
|
10084
10559
|
// `--force` is the operator saying "local wins": no direction
|
|
@@ -10203,6 +10678,7 @@ What a push guarantees (two stages):
|
|
|
10203
10678
|
// r-2 P1), and the plan itself was computed up-front so the final
|
|
10204
10679
|
// server var count could be validated against the cap before any
|
|
10205
10680
|
// mutation (review pass 5).
|
|
10681
|
+
const changedExistingVarKeys = [];
|
|
10206
10682
|
if (varPlan) {
|
|
10207
10683
|
skippedCount += varPlan.skippedCount;
|
|
10208
10684
|
// Feed var conflicts into the shared accumulator — same convention as
|
|
@@ -10305,6 +10781,12 @@ What a push guarantees (two stages):
|
|
|
10305
10781
|
contentHash: item.valueHash,
|
|
10306
10782
|
};
|
|
10307
10783
|
}
|
|
10784
|
+
// #4485 — a var that already existed and now holds another
|
|
10785
|
+
// value, for the cached-vars note after the function leg.
|
|
10786
|
+
const before = remoteVarsSnapshot.find((r) => r.key === item.key);
|
|
10787
|
+
if (before && before.value !== item.value) {
|
|
10788
|
+
changedExistingVarKeys.push(item.key);
|
|
10789
|
+
}
|
|
10308
10790
|
}
|
|
10309
10791
|
catch (err) {
|
|
10310
10792
|
// A remote edit landed since our snapshot — report a conflict
|
|
@@ -12276,7 +12758,7 @@ What a push guarantees (two stages):
|
|
|
12276
12758
|
let deferredAgentPromptFiles = [];
|
|
12277
12759
|
if (existsSync(promptsDir)) {
|
|
12278
12760
|
const files = selectFiles("prompt", promptsDir, readdirSync(promptsDir).filter((f) => f.endsWith(".toml")));
|
|
12279
|
-
const ordered = partitionPromptFilesForPush(promptsDir, files);
|
|
12761
|
+
const ordered = partitionPromptFilesForPush(promptsDir, files, preflightAgentFilesFirst);
|
|
12280
12762
|
deferredAgentPromptFiles = ordered.afterFunctions;
|
|
12281
12763
|
await pushPromptFiles(ordered.beforeFunctions);
|
|
12282
12764
|
}
|
|
@@ -13361,7 +13843,9 @@ What a push guarantees (two stages):
|
|
|
13361
13843
|
typeGateBlocked = true;
|
|
13362
13844
|
}
|
|
13363
13845
|
else {
|
|
13364
|
-
|
|
13846
|
+
// A fresh type's failure here is its create dry-run's, so it
|
|
13847
|
+
// is named a create, as the apply path names it.
|
|
13848
|
+
throw wrapEntityError(err, typeExistsOnServer ? "update" : "create", "database type", dbType);
|
|
13365
13849
|
}
|
|
13366
13850
|
}
|
|
13367
13851
|
// If the validate pass found a gate failure, record the change
|
|
@@ -13735,18 +14219,31 @@ What a push guarantees (two stages):
|
|
|
13735
14219
|
// consulted per function, so one broken file refuses itself and
|
|
13736
14220
|
// nothing else; `--no-typecheck` is the escape hatch, and an
|
|
13737
14221
|
// unavailable compiler warns rather than failing the push.
|
|
14222
|
+
const files = selectFiles("function", functionsPushDir, readdirSync(functionsPushDir).filter(isFunctionDeclarationFile));
|
|
14223
|
+
// #4502 — the directories outside the tree that function code may
|
|
14224
|
+
// import from, resolved once for the push so a dropped entry is
|
|
14225
|
+
// named once rather than once per function. The bundler and the
|
|
14226
|
+
// checker are handed the same list, so they agree on what is shared.
|
|
14227
|
+
const functionSourceRoots = files.length > 0
|
|
14228
|
+
? resolveFunctionSourceRoots(configDir)
|
|
14229
|
+
: { roots: [], warnings: [] };
|
|
14230
|
+
for (const warning of functionSourceRoots.warnings)
|
|
14231
|
+
warn(` ${warning}`);
|
|
14232
|
+
const sourceRoots = functionSourceRoots.roots.map((root) => root.absolute);
|
|
13738
14233
|
let typecheckSetup = null;
|
|
13739
14234
|
const functionTypechecker = () => {
|
|
13740
14235
|
if (options.typecheck === false)
|
|
13741
14236
|
return null;
|
|
13742
14237
|
if (typecheckSetup === null) {
|
|
13743
|
-
typecheckSetup = createFunctionTypechecker(configDir
|
|
14238
|
+
typecheckSetup = createFunctionTypechecker(configDir, {
|
|
14239
|
+
sourceRoots,
|
|
14240
|
+
onWarning: (message) => warn(` ${message}`),
|
|
14241
|
+
});
|
|
13744
14242
|
for (const warning of typecheckSetup.warnings)
|
|
13745
14243
|
warn(` ${warning}`);
|
|
13746
14244
|
}
|
|
13747
14245
|
return typecheckSetup.checker;
|
|
13748
14246
|
};
|
|
13749
|
-
const files = selectFiles("function", functionsPushDir, readdirSync(functionsPushDir).filter(isFunctionDeclarationFile));
|
|
13750
14247
|
const functionSpec = configDiffSpec("function");
|
|
13751
14248
|
const liveFunctions = new Map();
|
|
13752
14249
|
let functionsLiveOk = true;
|
|
@@ -13807,13 +14304,15 @@ What a push guarantees (two stages):
|
|
|
13807
14304
|
// Build first: nothing is sent for a function whose code does not
|
|
13808
14305
|
// compile or whose imports leave the tree.
|
|
13809
14306
|
let push;
|
|
14307
|
+
let projectSources;
|
|
13810
14308
|
try {
|
|
13811
|
-
push = await
|
|
14309
|
+
({ push, projectSources } = await buildFunctionConfigPushDetailed({
|
|
13812
14310
|
configDir,
|
|
13813
14311
|
functionKey: key,
|
|
13814
14312
|
filePath,
|
|
13815
14313
|
tomlData,
|
|
13816
|
-
|
|
14314
|
+
sourceRoots,
|
|
14315
|
+
}));
|
|
13817
14316
|
}
|
|
13818
14317
|
catch (err) {
|
|
13819
14318
|
if (!(err instanceof FunctionBuildError))
|
|
@@ -13843,7 +14342,7 @@ What a push guarantees (two stages):
|
|
|
13843
14342
|
// sent for it and whether or not this is a dry run.
|
|
13844
14343
|
const typechecker = functionTypechecker();
|
|
13845
14344
|
if (typechecker) {
|
|
13846
|
-
const failure = typechecker.check(key, push.sources.map((source) => source.path));
|
|
14345
|
+
const failure = typechecker.check(key, push.sources.map((source) => source.path), projectSources);
|
|
13847
14346
|
if (failure) {
|
|
13848
14347
|
applyFailures.push({
|
|
13849
14348
|
type: "function",
|
|
@@ -13933,6 +14432,11 @@ What a push guarantees (two stages):
|
|
|
13933
14432
|
const codeUnchanged = !!liveFunction &&
|
|
13934
14433
|
!!activeVersion &&
|
|
13935
14434
|
String(activeVersion.envelopeHash ?? "") === push.envelopeHash;
|
|
14435
|
+
// #4485 — the server makes no new version of it, so a running one
|
|
14436
|
+
// keeps the vars it has cached.
|
|
14437
|
+
functionKeysEvaluated.add(key);
|
|
14438
|
+
if (codeUnchanged)
|
|
14439
|
+
functionsWithUnchangedCode++;
|
|
13936
14440
|
if (headerAction === "skip" && codeUnchanged) {
|
|
13937
14441
|
skippedCount++;
|
|
13938
14442
|
if (syncState && liveFunction?.functionId) {
|
|
@@ -14015,6 +14519,7 @@ What a push guarantees (two stages):
|
|
|
14015
14519
|
type: "function",
|
|
14016
14520
|
key,
|
|
14017
14521
|
message: String(err?.message ?? err),
|
|
14522
|
+
transient: isTransientRequestFailure(err),
|
|
14018
14523
|
});
|
|
14019
14524
|
// A refused create, or a refused push to a row the server does
|
|
14020
14525
|
// not hold live (an archived tombstone). A row this push created
|
|
@@ -14847,6 +15352,7 @@ What a push guarantees (two stages):
|
|
|
14847
15352
|
type: "email-template",
|
|
14848
15353
|
key: emailType,
|
|
14849
15354
|
message: err?.message || String(err),
|
|
15355
|
+
transient: isTransientRequestFailure(err),
|
|
14850
15356
|
});
|
|
14851
15357
|
}
|
|
14852
15358
|
}
|
|
@@ -15284,6 +15790,27 @@ What a push guarantees (two stages):
|
|
|
15284
15790
|
}
|
|
15285
15791
|
}
|
|
15286
15792
|
}
|
|
15793
|
+
// #4485 (D4485-001) — an override changed a var some unchanged
|
|
15794
|
+
// function may already have read and cached. Informational only.
|
|
15795
|
+
if (varsOverrideFile && !options.dryRun && changedExistingVarKeys.length > 0) {
|
|
15796
|
+
let functionsKeepingVersion = functionsWithUnchangedCode;
|
|
15797
|
+
try {
|
|
15798
|
+
const listed = await client.listFunctions(resolvedAppId);
|
|
15799
|
+
for (const f of (listed?.items ?? [])) {
|
|
15800
|
+
if (f?.functionKey &&
|
|
15801
|
+
String(f.status ?? "active") !== "archived" &&
|
|
15802
|
+
!functionKeysEvaluated.has(f.functionKey)) {
|
|
15803
|
+
functionsKeepingVersion++;
|
|
15804
|
+
}
|
|
15805
|
+
}
|
|
15806
|
+
}
|
|
15807
|
+
catch {
|
|
15808
|
+
// Informational only: without the list, count what was evaluated.
|
|
15809
|
+
}
|
|
15810
|
+
const note = cachedVarsNote(changedExistingVarKeys, functionsKeepingVersion);
|
|
15811
|
+
if (note)
|
|
15812
|
+
warn(note);
|
|
15813
|
+
}
|
|
15287
15814
|
divider();
|
|
15288
15815
|
// Issue #813: a gate failure surfaced by the validate-first pass (which
|
|
15289
15816
|
// also runs in dry-run mode via the server's dry-run gate endpoints).
|
|
@@ -15362,6 +15889,17 @@ What a push guarantees (two stages):
|
|
|
15362
15889
|
else {
|
|
15363
15890
|
error(failureSummary);
|
|
15364
15891
|
}
|
|
15892
|
+
writePushFailureReport(pushFailureIsTransient({
|
|
15893
|
+
applyFailures,
|
|
15894
|
+
conflicts: conflicts.length,
|
|
15895
|
+
blockedDatabaseTypes: blockedDatabaseTypes.length,
|
|
15896
|
+
schemaErrors: schemaErrors.schemaRequired.length +
|
|
15897
|
+
schemaErrors.opRefs.length +
|
|
15898
|
+
schemaErrors.schemaBreaks.length +
|
|
15899
|
+
schemaErrors.uncheckableOps.length +
|
|
15900
|
+
schemaErrors.tomlParse.length +
|
|
15901
|
+
schemaErrors.opsExist.length,
|
|
15902
|
+
}));
|
|
15365
15903
|
process.exit(1);
|
|
15366
15904
|
}
|
|
15367
15905
|
else {
|
|
@@ -15402,6 +15940,7 @@ What a push guarantees (two stages):
|
|
|
15402
15940
|
// Print message + structured server-side validation details
|
|
15403
15941
|
// (issue #684; now via the shared #1173 helper).
|
|
15404
15942
|
printApiError(err);
|
|
15943
|
+
writePushFailureReport(isTransientRequestFailure(err));
|
|
15405
15944
|
process.exit(1);
|
|
15406
15945
|
}
|
|
15407
15946
|
});
|
|
@@ -15824,6 +16363,36 @@ What a push guarantees (two stages):
|
|
|
15824
16363
|
localVars.set(key, rawValue);
|
|
15825
16364
|
}
|
|
15826
16365
|
}
|
|
16366
|
+
// #4485 — on a machine-local environment, its own override layered
|
|
16367
|
+
// over vars.toml: the values its push would send.
|
|
16368
|
+
const diffEnv = requireCurrentEnvironment();
|
|
16369
|
+
const diffVarsOverridePath = resolveVarsOverridePath(configDir, diffEnv);
|
|
16370
|
+
let diffVarsOverride = null;
|
|
16371
|
+
if (diffVarsOverridePath && existsSync(diffVarsOverridePath)) {
|
|
16372
|
+
const parsed = parseTomlFile(diffVarsOverridePath);
|
|
16373
|
+
const baseKeys = new Set(existsSync(varsTomlPathDiff)
|
|
16374
|
+
? Object.keys(parseTomlFile(varsTomlPathDiff))
|
|
16375
|
+
: []);
|
|
16376
|
+
const keys = Object.keys(parsed).sort();
|
|
16377
|
+
const overridingKeys = keys.filter((key) => baseKeys.has(key));
|
|
16378
|
+
const addedKeys = keys.filter((key) => !baseKeys.has(key));
|
|
16379
|
+
if (varsOutcomeDiff.ok) {
|
|
16380
|
+
for (const [key, rawValue] of Object.entries(parsed)) {
|
|
16381
|
+
if (typeof rawValue === "string")
|
|
16382
|
+
localVars.set(key, rawValue);
|
|
16383
|
+
}
|
|
16384
|
+
}
|
|
16385
|
+
diffVarsOverride = {
|
|
16386
|
+
file: [PROJECT_TREE_DIR, diffEnv.tree, basename(diffVarsOverridePath)].join("/"),
|
|
16387
|
+
keys,
|
|
16388
|
+
};
|
|
16389
|
+
if (!options.json) {
|
|
16390
|
+
info(varsOverrideSummaryLine(basename(diffVarsOverridePath), {
|
|
16391
|
+
overridingKeys,
|
|
16392
|
+
addedKeys,
|
|
16393
|
+
}));
|
|
16394
|
+
}
|
|
16395
|
+
}
|
|
15827
16396
|
// Compare
|
|
15828
16397
|
let differences = [];
|
|
15829
16398
|
// Issue #1659: a local file with no server entity means one of two very
|
|
@@ -16384,7 +16953,9 @@ What a push guarantees (two stages):
|
|
|
16384
16953
|
}
|
|
16385
16954
|
}
|
|
16386
16955
|
// Config vars — issue #1423 (see `diffVars`).
|
|
16387
|
-
for (const row of diffVars(localVars, remoteVars
|
|
16956
|
+
for (const row of diffVars(localVars, remoteVars,
|
|
16957
|
+
// A child's pull writes its override, created or not (#4485).
|
|
16958
|
+
diffVarsOverridePath ? basename(diffVarsOverridePath) : "vars.toml")) {
|
|
16388
16959
|
differences.push(row);
|
|
16389
16960
|
}
|
|
16390
16961
|
// Database-config types (issue #1485). rule-sets, group/collection-type
|
|
@@ -16555,6 +17126,7 @@ What a push guarantees (two stages):
|
|
|
16555
17126
|
const { settings: localAppSettings, errors: appErrors } = buildAppSettingsPayload(appTomlData, {
|
|
16556
17127
|
excludeName,
|
|
16557
17128
|
nameOptional: diffsLocalEnvironment,
|
|
17129
|
+
excludeProtection: diffsLocalEnvironment,
|
|
16558
17130
|
});
|
|
16559
17131
|
if (appErrors.length > 0) {
|
|
16560
17132
|
appRow(VALIDATION_ERROR, appErrors.join("; "));
|
|
@@ -16569,7 +17141,7 @@ What a push guarantees (two stages):
|
|
|
16569
17141
|
});
|
|
16570
17142
|
}
|
|
16571
17143
|
else {
|
|
16572
|
-
const fieldDiffs = appSettingsFieldDiffs(localAppSettings, settingsOutcome.value ?? {}, { excludeName });
|
|
17144
|
+
const fieldDiffs = appSettingsFieldDiffs(localAppSettings, settingsOutcome.value ?? {}, { excludeName, excludeProtection: diffsLocalEnvironment });
|
|
16573
17145
|
if (fieldDiffs.length === 0) {
|
|
16574
17146
|
appRow("exists");
|
|
16575
17147
|
}
|
|
@@ -16647,6 +17219,7 @@ What a push guarantees (two stages):
|
|
|
16647
17219
|
differences,
|
|
16648
17220
|
testCases: testCaseDiffs,
|
|
16649
17221
|
notCompared,
|
|
17222
|
+
...(diffVarsOverride ? { varsOverride: diffVarsOverride } : {}),
|
|
16650
17223
|
}, null, 2));
|
|
16651
17224
|
return;
|
|
16652
17225
|
}
|