@orchestraworks/cli 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/api/approval.js +3 -3
- package/dist/cli/commands/outpost/local.js +3 -3
- package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
- package/dist/cli/commands/outpost/machine-names.js +22 -0
- package/dist/cli/commands/outpost/up.js +7 -5
- package/dist/cli/commands/worker/enroll.d.ts +4 -3
- package/dist/cli/commands/worker/enroll.js +4 -3
- package/dist/cli/core/auth.d.ts +22 -2
- package/dist/cli/core/auth.js +69 -8
- package/dist/cli/core/errors.js +4 -0
- package/dist/cli/core/platform-client.d.ts +3 -2
- package/dist/cli/core/session-body.d.ts +1 -1
- package/dist/cli/core/session-body.js +1 -1
- package/dist/cli/firstRun.d.ts +3 -3
- package/dist/cli/firstRun.js +7 -5
- package/dist/cli/main.js +4 -3
- package/dist/constructs/environment.d.ts +24 -11
- package/dist/constructs/environment.js +76 -12
- package/dist/constructs/secret.d.ts +17 -7
- package/dist/constructs/secret.js +24 -16
- package/dist/core/labels.d.ts +32 -6
- package/dist/core/labels.js +70 -8
- package/dist/engine/diff.js +38 -11
- package/dist/engine/gateway-key-reconcile.d.ts +1 -2
- package/dist/engine/gateway-key-reconcile.js +19 -15
- package/dist/engine/pipeline-reconcile.js +2 -2
- package/dist/engine/pipeline-workflow-reconcile.js +15 -9
- package/dist/engine/reconcile.js +48 -15
- package/dist/engine/refusals.d.ts +0 -5
- package/dist/engine/refusals.js +1 -5
- package/dist/engine/secrets-apply.d.ts +10 -14
- package/dist/engine/secrets-apply.js +12 -17
- package/dist/engine/workflow-reconcile.d.ts +1 -1
- package/dist/generated/cli-commands.json +170 -34
- package/dist/generated/providers.d.ts +40 -0
- package/dist/generated/providers.js +30 -0
- package/docs/REFERENCE.md +26 -12
- package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/package.json +1 -1
- package/package.json +4 -4
- package/skills/orchestra/SKILL.md +38 -17
- package/skills/orchestra-fdk/references/authoring.md +34 -26
- package/skills/orchestra-fdk/references/environment.md +49 -12
- package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
- package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
- package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
- package/skills/orchestra-fdk/references/troubleshooting.md +5 -3
- package/templates/bug-fix/main.ts +7 -4
- package/templates/engineering-team/lib/environment.ts +6 -3
- package/templates/engineering-team/package.json +1 -1
- package/templates/todo-factory/main.ts +7 -4
- package/templates/todo-factory/package.json +1 -1
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
import { gatewayKeyRefOf } from "../core/gateway-key-recipe.js";
|
|
14
14
|
import { refToken } from "../core/tokens.js";
|
|
15
15
|
import { credentialValueWritten, driftedFields, failing, getBySlugOrUndefined, pushOutcome, stampTeam, } from "./apply-core.js";
|
|
16
|
-
import {
|
|
16
|
+
import { refuseIf } from "./refusals.js";
|
|
17
17
|
import { httpStatusOf } from "./client.js";
|
|
18
18
|
import { PreflightError } from "./preflight.js";
|
|
19
|
-
import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
|
|
20
|
-
import {
|
|
19
|
+
import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY, oldMarkersCleared, oldMarkersMoved, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, readMarker, } from "../core/labels.js";
|
|
20
|
+
import { declaredSecretInjection, metadataInjectionChange, pendingSecretWrite, reconcileInjection, } from "./secrets-apply.js";
|
|
21
21
|
/**
|
|
22
22
|
* The reserved marker rides the ordinary labels wire field — never smuggled into the write-only value or a
|
|
23
23
|
* second column — and is removed by the final token PATCH. It lives in a leaf module because the aggregate
|
|
24
24
|
* refusal classifier's rotation message names it too.
|
|
25
25
|
*/
|
|
26
|
-
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "
|
|
26
|
+
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "../core/labels.js";
|
|
27
27
|
/**
|
|
28
28
|
* The platform requires a non-empty `value` on create and the token does not exist yet, so a reservation
|
|
29
29
|
* writes this inert literal and the final PATCH replaces it. The Secret is detectably pending for exactly as
|
|
@@ -149,14 +149,17 @@ async function applySteadyGatewayKey(decision, apis, outcomes, applied) {
|
|
|
149
149
|
action = "updated";
|
|
150
150
|
}
|
|
151
151
|
pushOutcome(outcomes, applied, decision.resource, live.id, action);
|
|
152
|
-
// The paired Secret needs no value write in steady state, but a recipe
|
|
152
|
+
// The paired Secret needs no value write in steady state, but a recipe edit, its hosts included, still reconciles
|
|
153
153
|
// through a value-less PATCH — exactly what `applySecrets` would have done had this lane not owned it.
|
|
154
154
|
const liveSecret = decision.liveSecret;
|
|
155
155
|
const injectionChange = metadataInjectionChange(secretInjection(decision.secretResource), liveSecret.injection);
|
|
156
|
-
const
|
|
156
|
+
const labelsMove = oldMarkersMoved(liveSecret.labels);
|
|
157
157
|
let secretAction = "unchanged";
|
|
158
|
-
if (injectionChange !== undefined ||
|
|
159
|
-
const body = {
|
|
158
|
+
if (injectionChange !== undefined || labelsMove !== undefined) {
|
|
159
|
+
const body = {
|
|
160
|
+
...injectionChange,
|
|
161
|
+
...(labelsMove !== undefined ? { labels: labelsMove } : {}),
|
|
162
|
+
};
|
|
160
163
|
await failing(() => apis.secrets.updateSecret({ id: liveSecret.id, updateSecretRequest: body }), { kind: "secret", slug: decision.secretSlug, op: "update" }, outcomes);
|
|
161
164
|
secretAction = "updated";
|
|
162
165
|
}
|
|
@@ -174,9 +177,12 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
174
177
|
const refKey = refToken("gateway-key", decision.resource.path.split("/"));
|
|
175
178
|
// The FINAL patch removes the pending marker and stamps ownership; the merge keeps every caller-authored entry
|
|
176
179
|
// an adopted Secret carries.
|
|
177
|
-
const finalLabels = {
|
|
180
|
+
const finalLabels = {
|
|
181
|
+
[MANAGED_BY_LABEL_KEY]: factory,
|
|
182
|
+
[PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: null,
|
|
183
|
+
...oldMarkersCleared(decision.liveSecret?.labels),
|
|
184
|
+
};
|
|
178
185
|
const declaredInjection = secretInjection(decision.secretResource);
|
|
179
|
-
const declaredHosts = declaredSecretHosts(decision.secretResource.payload);
|
|
180
186
|
// (1) Make the paired Secret detectably pending BEFORE the mint.
|
|
181
187
|
let secretId;
|
|
182
188
|
if (decision.action === "mint-new") {
|
|
@@ -188,7 +194,6 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
188
194
|
labels: { [PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: decision.slug },
|
|
189
195
|
...placement,
|
|
190
196
|
...(declaredInjection !== undefined ? { injection: declaredInjection } : {}),
|
|
191
|
-
...(declaredHosts !== undefined ? { hosts: declaredHosts } : {}),
|
|
192
197
|
};
|
|
193
198
|
const created = await failing(() => apis.secrets.createSecret({ createSecretRequest: createBody }), { kind: "secret", slug: decision.secretSlug, op: "create" }, outcomes);
|
|
194
199
|
secretId = created.id;
|
|
@@ -202,6 +207,7 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
202
207
|
await patchSecretPendingMarker(apis, outcomes, decision.secretSlug, secretId, {
|
|
203
208
|
[MANAGED_BY_LABEL_KEY]: factory,
|
|
204
209
|
[PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: decision.slug,
|
|
210
|
+
...oldMarkersCleared(liveSecret.labels),
|
|
205
211
|
});
|
|
206
212
|
}
|
|
207
213
|
}
|
|
@@ -237,8 +243,6 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
237
243
|
// No team: a `mint-new` reservation already placed the create, and an adopted or resumed Secret is
|
|
238
244
|
// only here because the decide phase found it already in the declared team.
|
|
239
245
|
...(injection !== undefined ? { injection } : {}),
|
|
240
|
-
// A `mint-new` reservation already carried the hosts on its create.
|
|
241
|
-
...(decision.action !== "mint-new" && declaredHosts !== undefined ? { hosts: declaredHosts } : {}),
|
|
242
246
|
},
|
|
243
247
|
}), { kind: "secret", slug: decision.secretSlug, op: "update" }, outcomes);
|
|
244
248
|
// The pinned asymmetry between the pair's two rows: the `gateway-key` row is the MINT, whose sensitivity
|
|
@@ -277,7 +281,7 @@ function pairedSecretResource(plan, key) {
|
|
|
277
281
|
return paired[0];
|
|
278
282
|
}
|
|
279
283
|
function hasPendingMarker(secret) {
|
|
280
|
-
return secret.labels
|
|
284
|
+
return readMarker(secret.labels, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY) !== undefined;
|
|
281
285
|
}
|
|
282
286
|
/**
|
|
283
287
|
* Exactly the fields the update request accepts and the construct emits. `monthlyCreditCap` is present only
|
|
@@ -293,5 +297,5 @@ function declaredKeyState(payload) {
|
|
|
293
297
|
};
|
|
294
298
|
}
|
|
295
299
|
function secretInjection(secretResource) {
|
|
296
|
-
return secretResource.payload
|
|
300
|
+
return declaredSecretInjection(secretResource.payload);
|
|
297
301
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { OPEN_CATEGORIES } from "../constructs/pipeline.js";
|
|
12
12
|
import { resolveConditionReferences, resolveStageInField } from "./condition-references.js";
|
|
13
13
|
import { wireNode } from "../constructs/workflow-filter.js";
|
|
14
|
-
import { createLabels, labelsPatch,
|
|
14
|
+
import { createLabels, labelsPatch, managedByOf } from "../core/labels.js";
|
|
15
15
|
import { refToken } from "../core/tokens.js";
|
|
16
16
|
import { exactDeepEqual, failing, getBySlugOrUndefined, stampTeam, } from "./apply-core.js";
|
|
17
17
|
import { PreflightError } from "./preflight-errors.js";
|
|
@@ -242,7 +242,7 @@ export async function workflowStagesAfterApply(pipeline, live, resources, factor
|
|
|
242
242
|
const versions = [
|
|
243
243
|
...declaredEntries.filter((entry) => refs.has(entry.workflow)).map((entry) => entry.version),
|
|
244
244
|
...liveEntries
|
|
245
|
-
.filter((entry) => entry.labels
|
|
245
|
+
.filter((entry) => managedByOf(entry.labels) !== factory && refs.has(entry.workflow.id))
|
|
246
246
|
.map((entry) => entry.version ?? null),
|
|
247
247
|
];
|
|
248
248
|
if (versions.some((version) => typeof version === "number"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A pipeline's workflow entries. Apply owns only the entries it stamped with this factory's
|
|
1
|
+
// A pipeline's workflow entries. Apply owns only the entries it stamped with this factory's managed-by marker:
|
|
2
2
|
// each declared entry takes a stamped live entry of its workflow, an identical one first and then the next in
|
|
3
3
|
// order, and a changed one is patched field by field; a declared entry left over is added with the stamp, and
|
|
4
4
|
// a stamped entry nothing declares is removed. Every other entry stays where it is and is named, since an
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// new rules before its entry says so; the entries phase then sets each to its declared version.
|
|
10
10
|
import { APPLIED_VERSION } from "../constructs/pipeline.js";
|
|
11
11
|
import { wireNode } from "../constructs/workflow-filter.js";
|
|
12
|
-
import { MANAGED_BY_LABEL_KEY } from "../core/labels.js";
|
|
12
|
+
import { MANAGED_BY_LABEL_KEY, managedByOf, oldMarkersCleared } from "../core/labels.js";
|
|
13
13
|
import { exactDeepEqual, failing, getBySlugOrUndefined } from "./apply-core.js";
|
|
14
14
|
import { rawJsonBody } from "./raw-json-request.js";
|
|
15
15
|
export function entryChange(declared, live, factory) {
|
|
16
|
-
const stamped = live.filter((entry) => entry.labels
|
|
16
|
+
const stamped = live.filter((entry) => managedByOf(entry.labels) === factory);
|
|
17
17
|
const taken = new Set();
|
|
18
18
|
const take = (entry, fits) => {
|
|
19
19
|
const found = stamped.find((candidate) => !taken.has(candidate.id) && names(entry, candidate) && fits(candidate));
|
|
@@ -21,18 +21,18 @@ export function entryChange(declared, live, factory) {
|
|
|
21
21
|
taken.add(found.id);
|
|
22
22
|
return found;
|
|
23
23
|
};
|
|
24
|
-
const exact = declared.map((entry) => take(entry, (candidate) => patchOf(entry, candidate) === undefined));
|
|
24
|
+
const exact = declared.map((entry) => take(entry, (candidate) => patchOf(entry, candidate, factory) === undefined));
|
|
25
25
|
const matched = declared.map((entry, index) => exact[index] ?? take(entry, () => true));
|
|
26
26
|
return {
|
|
27
27
|
declared: declared.map((entry, index) => ({ entry, ...(matched[index] ? { liveId: matched[index].id } : {}) })),
|
|
28
28
|
create: declared.filter((_, index) => matched[index] === undefined),
|
|
29
29
|
update: declared.flatMap((entry, index) => {
|
|
30
30
|
const live = matched[index];
|
|
31
|
-
const patch = live !== undefined ? patchOf(entry, live) : undefined;
|
|
31
|
+
const patch = live !== undefined ? patchOf(entry, live, factory) : undefined;
|
|
32
32
|
return live !== undefined && patch !== undefined ? [{ id: live.id, patch }] : [];
|
|
33
33
|
}),
|
|
34
34
|
remove: stamped.filter((entry) => !taken.has(entry.id)),
|
|
35
|
-
unmanaged: live.filter((entry) => entry.labels
|
|
35
|
+
unmanaged: live.filter((entry) => managedByOf(entry.labels) !== factory),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
export function entriesUnchanged(change) {
|
|
@@ -87,7 +87,7 @@ export async function applyEntriesPhase(apis, pipelines, versions, factory, outc
|
|
|
87
87
|
*/
|
|
88
88
|
export async function pinFollowingEntries(apis, pipeline, workflow, version, factory) {
|
|
89
89
|
const live = await getBySlugOrUndefined(() => apis.pipelines.getPipeline({ id: pipeline }));
|
|
90
|
-
const following = (live?.workflows ?? []).filter((entry) => entry.labels
|
|
90
|
+
const following = (live?.workflows ?? []).filter((entry) => managedByOf(entry.labels) === factory && entry.workflow.id === workflow.id && entry.version === undefined);
|
|
91
91
|
for (const entry of following)
|
|
92
92
|
await patchEntry(apis, live.id, entry.id, { version });
|
|
93
93
|
return following.length;
|
|
@@ -165,9 +165,15 @@ export function describeOverlaps(proposedList, overlaps) {
|
|
|
165
165
|
function names(entry, live) {
|
|
166
166
|
return entry.workflow === live.workflow.slug || entry.workflow === live.workflow.id;
|
|
167
167
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
168
|
+
/**
|
|
169
|
+
* The fields a patch must send to make [live] the declared entry, null resetting; `undefined` when none. An entry
|
|
170
|
+
* stamped under the old prefix has its stamp moved.
|
|
171
|
+
*/
|
|
172
|
+
function patchOf(entry, live, factory) {
|
|
170
173
|
const patch = {};
|
|
174
|
+
const moved = oldMarkersCleared(live.labels);
|
|
175
|
+
if (Object.keys(moved).length > 0)
|
|
176
|
+
patch.labels = { ...moved, [MANAGED_BY_LABEL_KEY]: factory };
|
|
171
177
|
if (entry.version !== (live.version ?? null))
|
|
172
178
|
patch.version = entry.version;
|
|
173
179
|
const where = entry.where === null ? null : wireNode(entry.where);
|
package/dist/engine/reconcile.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { CONNECTOR_SECRET_WIRE_FIELDS, connectorImmutableFields, } from "../constructs/connector.js";
|
|
7
7
|
import { isEnvImportMarker, parseEnvImportMarker } from "../constructs/references.js";
|
|
8
8
|
import { isGatewayKeyRecipe } from "../core/gateway-key-recipe.js";
|
|
9
|
-
import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY, stampManagedBy } from "../core/labels.js";
|
|
9
|
+
import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY, oldMarkersMoved, stampManagedBy } from "../core/labels.js";
|
|
10
10
|
import { isRefToken, parseRefToken, refToken } from "../core/tokens.js";
|
|
11
11
|
import { ApplyError, credentialValueWritten, defaultPrint, createTeamResolver, describeTargetOrganization, failing, getBySlugOrUndefined, modelSettingsUnchanged, stampTeam, standbyCapacityUnchanged, subsetDeepEqual, } from "./apply-core.js";
|
|
12
12
|
import { preApprovalLevers, typedAtTerminalRemedy, } from "../cli/core/approval-policy.js";
|
|
@@ -22,7 +22,7 @@ import { applyCeremonyBackedConnection, isCeremonyBackedConnection, } from "./co
|
|
|
22
22
|
import { assertManagedChatConnectionForDesired } from "./connection-readiness.js";
|
|
23
23
|
import { removedConfigurationNotice } from "./connector-configuration.js";
|
|
24
24
|
import { PreflightError, PreflightRefusalError, readConnection, RedactedRefError, reservedProviderSecretIds, resolveSecretRef, validateConnectionImport, validateRefShape, } from "./preflight.js";
|
|
25
|
-
import { allRootSourceLabels, commandLineOccurrencesFor, containsCommandLineRecipe,
|
|
25
|
+
import { allRootSourceLabels, commandLineOccurrencesFor, containsCommandLineRecipe, declaredSecretInjection, decideSecretWrite, metadataInjectionChange, pendingSecretWrite, reconcileInjection, replacesLiveCredentialValue, resolveSecretValueSource, } from "./secrets-apply.js";
|
|
26
26
|
import { collectCommandLineValues, } from "./command-line-values.js";
|
|
27
27
|
// These live in the leaf module `engine/apply-core.ts` so the per-kind reconcile lanes can share them
|
|
28
28
|
// without importing this monolith, which imports THEM — a cycle. Re-exported here so `engine/reconcile`
|
|
@@ -185,7 +185,7 @@ export async function reconcile(plan, apis, opts, appliedOut) {
|
|
|
185
185
|
await applyComputeProvider(resource, apis, outcomes, applied, opts.onComputeProviderKey);
|
|
186
186
|
break;
|
|
187
187
|
case "environment":
|
|
188
|
-
await applyEnvironment(resource, apis, outcomes, applied, reserved, teams);
|
|
188
|
+
await applyEnvironment(resource, apis, outcomes, applied, connections, reserved, teams);
|
|
189
189
|
break;
|
|
190
190
|
case "skill":
|
|
191
191
|
await applySkill(resource, apis, outcomes, applied, teams);
|
|
@@ -376,14 +376,14 @@ async function applySecrets(plan, apis, opts, env, outcomes, applied, teams, gat
|
|
|
376
376
|
? undefined
|
|
377
377
|
: resolveSecretValueSource(resource.valueFrom, env);
|
|
378
378
|
// Diffed against the live recipe so a value-less recipe edit still PATCHes instead of silently no-oping.
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
const
|
|
379
|
+
const injectionChange = metadataInjectionChange(declaredSecretInjection(resource.payload), live?.injection);
|
|
380
|
+
// A marker an apply before the rename stamped moves to the new prefix even when nothing else changes.
|
|
381
|
+
const labelsMove = oldMarkersMoved(live?.labels);
|
|
382
382
|
const decision = decideSecretWrite(resolvedValue, live !== undefined, {
|
|
383
383
|
slug,
|
|
384
384
|
envName,
|
|
385
385
|
injectionChange,
|
|
386
|
-
|
|
386
|
+
...(labelsMove !== undefined ? { labelsChange: { labels: labelsMove } } : {}),
|
|
387
387
|
commandLinePending,
|
|
388
388
|
});
|
|
389
389
|
if (decision.kind === "skip-empty") {
|
|
@@ -399,6 +399,7 @@ async function applySecrets(plan, apis, opts, env, outcomes, applied, teams, gat
|
|
|
399
399
|
value: resolvedValue,
|
|
400
400
|
id: live?.id,
|
|
401
401
|
liveInjection: live?.injection,
|
|
402
|
+
labelsMove,
|
|
402
403
|
replacesLiveValue: replacesLiveCredentialValue(resource.valueFrom, decision.kind === "overwrite" ? "overwrite" : "create"),
|
|
403
404
|
});
|
|
404
405
|
}
|
|
@@ -481,8 +482,7 @@ async function applySecrets(plan, apis, opts, env, outcomes, applied, teams, gat
|
|
|
481
482
|
}
|
|
482
483
|
// Write phase — creates and overwrites, in plan order.
|
|
483
484
|
for (const p of secretPlans) {
|
|
484
|
-
const desiredInjection = p.resource.payload
|
|
485
|
-
const desiredHosts = declaredSecretHosts(p.resource.payload);
|
|
485
|
+
const desiredInjection = declaredSecretInjection(p.resource.payload);
|
|
486
486
|
if (p.decision.kind === "create") {
|
|
487
487
|
const placement = {};
|
|
488
488
|
stampTeam(placement, p.team);
|
|
@@ -498,7 +498,6 @@ async function applySecrets(plan, apis, opts, env, outcomes, applied, teams, gat
|
|
|
498
498
|
...placement,
|
|
499
499
|
// Live is absent, so never a `null` clear here.
|
|
500
500
|
...(desiredInjection !== undefined ? { injection: desiredInjection } : {}),
|
|
501
|
-
...(desiredHosts !== undefined ? { hosts: desiredHosts } : {}),
|
|
502
501
|
};
|
|
503
502
|
const created = await failing(() => apis.secrets.createSecret({ createSecretRequest: body }), { kind: "secret", slug: p.slug, op: "create" }, outcomes);
|
|
504
503
|
p.id = created.id;
|
|
@@ -513,16 +512,16 @@ async function applySecrets(plan, apis, opts, env, outcomes, applied, teams, gat
|
|
|
513
512
|
description: p.resource.payload.description,
|
|
514
513
|
value: p.value,
|
|
515
514
|
...(injection !== undefined ? { injection } : {}),
|
|
516
|
-
...(
|
|
515
|
+
...(p.labelsMove !== undefined ? { labels: p.labelsMove } : {}),
|
|
517
516
|
};
|
|
518
517
|
await failing(() => apis.secrets.updateSecret({ id: p.id, updateSecretRequest: body }), { kind: "secret", slug: p.slug, op: "update" }, outcomes);
|
|
519
518
|
}
|
|
520
519
|
else if (p.decision.kind === "metadata-update") {
|
|
521
|
-
// The secret exists with no fresh value but its recipe or
|
|
522
|
-
//
|
|
520
|
+
// The secret exists with no fresh value but its recipe changed or an old marker moves: a value-less PATCH
|
|
521
|
+
// carrying the recipe and the label move alone.
|
|
523
522
|
const body = {
|
|
524
523
|
...(p.decision.injection !== undefined ? { injection: p.decision.injection } : {}),
|
|
525
|
-
...(p.decision.
|
|
524
|
+
...(p.decision.labels !== undefined ? { labels: p.decision.labels } : {}),
|
|
526
525
|
};
|
|
527
526
|
await failing(() => apis.secrets.updateSecret({ id: p.id, updateSecretRequest: body }), { kind: "secret", slug: p.slug, op: "update" }, outcomes);
|
|
528
527
|
}
|
|
@@ -839,7 +838,26 @@ function connectionExpectFor(imports, ref) {
|
|
|
839
838
|
}
|
|
840
839
|
return { provider, install: Object.keys(install).length > 0 ? install : undefined };
|
|
841
840
|
}
|
|
842
|
-
|
|
841
|
+
/** The prefix an env value carries before a credential reference, which the egress edge reads. */
|
|
842
|
+
const ENV_REFERENCE_PREFIX = "orchestra:";
|
|
843
|
+
const CREDENTIAL_REFERENCE = /^(secret|connection)\/(.+)$/;
|
|
844
|
+
/**
|
|
845
|
+
* `secret/<ref>` or `connection/<ref>` with its ref, a reference token or an import as written, resolved to
|
|
846
|
+
* the live id: the edge and the platform read an exact id, never a slug. Text naming neither is left as it is.
|
|
847
|
+
*/
|
|
848
|
+
async function resolveCredentialReference(text, label, apis, applied, connections) {
|
|
849
|
+
const match = CREDENTIAL_REFERENCE.exec(text);
|
|
850
|
+
if (match === null)
|
|
851
|
+
return text;
|
|
852
|
+
const [, kind, ref] = match;
|
|
853
|
+
if (kind === "connection")
|
|
854
|
+
return `connection/${await connections.resolve(ref)}`;
|
|
855
|
+
if (isRefToken(ref))
|
|
856
|
+
return `secret/${resolveManagedToken(ref, applied)}`;
|
|
857
|
+
validateRefShape(ref, label);
|
|
858
|
+
return `secret/${(await resolveSecretRef(apis, { value: ref, sourceLabel: label })).id}`;
|
|
859
|
+
}
|
|
860
|
+
async function applyEnvironment(resource, apis, outcomes, applied, connections, reserved, teams) {
|
|
843
861
|
const body = clonePayload(resource.payload);
|
|
844
862
|
const team = await teams.desired(body.teamId, { kind: "environment", slug: resource.slug, op: "get" }, outcomes);
|
|
845
863
|
stampTeam(body, team);
|
|
@@ -884,6 +902,21 @@ async function applyEnvironment(resource, apis, outcomes, applied, reserved, tea
|
|
|
884
902
|
reservedProviderSecretIds: await reserved(),
|
|
885
903
|
}, outcomes);
|
|
886
904
|
}
|
|
905
|
+
// Each credential an env value or a default names becomes its id before anything is written.
|
|
906
|
+
const env = (launchSpec.env ?? {});
|
|
907
|
+
for (const [name, value] of Object.entries(env)) {
|
|
908
|
+
if (!value.startsWith(ENV_REFERENCE_PREFIX))
|
|
909
|
+
continue;
|
|
910
|
+
const label = `environment '${envPath}' env '${name}'`;
|
|
911
|
+
env[name] =
|
|
912
|
+
ENV_REFERENCE_PREFIX +
|
|
913
|
+
(await resolveCredentialReference(value.slice(ENV_REFERENCE_PREFIX.length), label, apis, applied, connections));
|
|
914
|
+
}
|
|
915
|
+
const defaults = (body.credentials ?? []);
|
|
916
|
+
for (const [index, entry] of defaults.entries()) {
|
|
917
|
+
const label = `environment '${envPath}' credentials[${index}]`;
|
|
918
|
+
entry.credential = await resolveCredentialReference(String(entry.credential), label, apis, applied, connections);
|
|
919
|
+
}
|
|
887
920
|
// A declared record's token, or an imported slug, becomes the record's id; `null` stays, naming hosted
|
|
888
921
|
// compute.
|
|
889
922
|
const providerRef = launchSpec.computeProviderId;
|
|
@@ -144,10 +144,5 @@ export declare function byokUnsetSourceMessage(slug: string, modelProvider: stri
|
|
|
144
144
|
* apply lane reaches through {@link refusalOf}.
|
|
145
145
|
*/
|
|
146
146
|
export declare function gatewayKeyPairRefusal(keySlug: string, secretSlug: string, pair: LiveGatewayKeyPair, team: DesiredTeam | undefined, pairContext: TeamIdentityRefusal["pairContext"] | undefined): RefusalVerdict | null;
|
|
147
|
-
/**
|
|
148
|
-
* Lives here rather than in the gateway-key lane because the classifier's message names it and this module
|
|
149
|
-
* must stay a leaf; the lane re-exports it.
|
|
150
|
-
*/
|
|
151
|
-
export declare const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = "sfdk.pendingGatewayKeyToken";
|
|
152
147
|
/** The named refusal for a mint that succeeded but whose paired-Secret token write did not. */
|
|
153
148
|
export declare function pendingGatewayKeyTokenRotationMessage(slug: string, secretSlug: string): string;
|
package/dist/engine/refusals.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// nothing rather than guessing. No verdict can carry a credential: the classifiers read source RECIPES.
|
|
13
13
|
import { connectorImmutableFields } from "../constructs/connector.js";
|
|
14
14
|
import { isGatewayKeyRecipe } from "../core/gateway-key-recipe.js";
|
|
15
|
+
import { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "../core/labels.js";
|
|
15
16
|
import { ApplyError, classifyTeamIdentity, subsetDeepEqual, teamIdentityConflict, } from "./apply-core.js";
|
|
16
17
|
import { isPartiallySuppliedRecipe, missingRootEnvNames, pendingCredentialWrite, } from "./secrets-apply.js";
|
|
17
18
|
import { managedChatConnectionRefusalMessage } from "./connection-readiness.js";
|
|
@@ -266,11 +267,6 @@ function pairedSecretAbsentMessage(slug, secretSlug) {
|
|
|
266
267
|
return (`gateway key '${slug}': the key already exists but its paired secret '${secretSlug}' does not, so ` +
|
|
267
268
|
`there is no token to write and none can be recovered. ${gatewayKeyRotationRemedy(slug)}`);
|
|
268
269
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Lives here rather than in the gateway-key lane because the classifier's message names it and this module
|
|
271
|
-
* must stay a leaf; the lane re-exports it.
|
|
272
|
-
*/
|
|
273
|
-
export const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = "sfdk.pendingGatewayKeyToken";
|
|
274
270
|
/** The named refusal for a mint that succeeded but whose paired-Secret token write did not. */
|
|
275
271
|
export function pendingGatewayKeyTokenRotationMessage(slug, secretSlug) {
|
|
276
272
|
return (`gateway key '${slug}': its paired secret '${secretSlug}' still carries the pending marker ` +
|
|
@@ -129,21 +129,17 @@ export declare function metadataInjectionChange(desired: InjectionRecipe | undef
|
|
|
129
129
|
injection: InjectionRecipe | null;
|
|
130
130
|
} | undefined;
|
|
131
131
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* `undefined` when the plan states no hosts at all (a plan written before the field) or they already match.
|
|
132
|
+
* A declared secret's recipe as the plan carries it, its hosts inside it. A plan written while hosts sat beside the
|
|
133
|
+
* recipe has them moved in, so it applies as it did; with no recipe they were never presentable and are dropped.
|
|
135
134
|
*/
|
|
136
|
-
export declare function
|
|
137
|
-
hosts: string[];
|
|
138
|
-
} | undefined;
|
|
139
|
-
/** A declared secret's hosts as the plan carries them; `undefined` for a plan written before the field. */
|
|
140
|
-
export declare function declaredSecretHosts(payload: unknown): string[] | undefined;
|
|
135
|
+
export declare function declaredSecretInjection(payload: unknown): InjectionRecipe | undefined;
|
|
141
136
|
/**
|
|
142
137
|
* The write kinds carry value-free identifiers for the confirmation prompt, never the value itself.
|
|
143
138
|
* - `overwrite` — values are never readable, so any supplied value overwrites.
|
|
144
139
|
* - `noop-resolve` — resolve the id and write nothing, which protects the live credential from an
|
|
145
140
|
* empty overwrite.
|
|
146
|
-
* - `metadata-update` — a recipe
|
|
141
|
+
* - `metadata-update` — a recipe edit, its hosts included, changes no secret VALUE, so it must not be dropped as
|
|
142
|
+
* a noop.
|
|
147
143
|
* - `skip-empty` — the reconcile engine turns this into the named preflight error.
|
|
148
144
|
*/
|
|
149
145
|
export type SecretDecision = {
|
|
@@ -159,13 +155,13 @@ export type SecretDecision = {
|
|
|
159
155
|
} | {
|
|
160
156
|
kind: "metadata-update";
|
|
161
157
|
injection?: InjectionRecipe | null;
|
|
162
|
-
|
|
158
|
+
labels?: Record<string, string | null>;
|
|
163
159
|
} | {
|
|
164
160
|
kind: "skip-empty";
|
|
165
161
|
};
|
|
166
162
|
/**
|
|
167
|
-
* `meta.injectionChange` and `meta.
|
|
168
|
-
*
|
|
163
|
+
* `meta.injectionChange` and `meta.labelsChange` steer only the no-value-plus-exists branch, since a fresh value
|
|
164
|
+
* already writes the recipe and the label move through the create/overwrite body.
|
|
169
165
|
*
|
|
170
166
|
* `meta.commandLinePending` is the value-free branch, and it is load-bearing: a `cli:`-backed recipe has no
|
|
171
167
|
* bytes yet when this runs, because the collection pass deliberately happens AFTER the confirmation this
|
|
@@ -179,8 +175,8 @@ export declare function decideSecretWrite(resolvedValue: string | undefined, exi
|
|
|
179
175
|
injectionChange?: {
|
|
180
176
|
injection: InjectionRecipe | null;
|
|
181
177
|
};
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
labelsChange?: {
|
|
179
|
+
labels: Record<string, string | null>;
|
|
184
180
|
};
|
|
185
181
|
commandLinePending?: boolean;
|
|
186
182
|
}): SecretDecision;
|
|
@@ -306,25 +306,20 @@ export function metadataInjectionChange(desired, live) {
|
|
|
306
306
|
return live !== undefined ? { injection: null } : undefined;
|
|
307
307
|
}
|
|
308
308
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* `undefined` when the plan states no hosts at all (a plan written before the field) or they already match.
|
|
309
|
+
* A declared secret's recipe as the plan carries it, its hosts inside it. A plan written while hosts sat beside the
|
|
310
|
+
* recipe has them moved in, so it applies as it did; with no recipe they were never presentable and are dropped.
|
|
312
311
|
*/
|
|
313
|
-
export function
|
|
314
|
-
|
|
312
|
+
export function declaredSecretInjection(payload) {
|
|
313
|
+
const { injection, hosts } = payload;
|
|
314
|
+
if (injection === undefined)
|
|
315
315
|
return undefined;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return
|
|
319
|
-
}
|
|
320
|
-
/** A declared secret's hosts as the plan carries them; `undefined` for a plan written before the field. */
|
|
321
|
-
export function declaredSecretHosts(payload) {
|
|
322
|
-
const hosts = payload.hosts;
|
|
323
|
-
return Array.isArray(hosts) ? hosts : undefined;
|
|
316
|
+
if (injection.hosts !== undefined || !Array.isArray(hosts))
|
|
317
|
+
return injection;
|
|
318
|
+
return { ...injection, hosts: [...hosts] };
|
|
324
319
|
}
|
|
325
320
|
/**
|
|
326
|
-
* `meta.injectionChange` and `meta.
|
|
327
|
-
*
|
|
321
|
+
* `meta.injectionChange` and `meta.labelsChange` steer only the no-value-plus-exists branch, since a fresh value
|
|
322
|
+
* already writes the recipe and the label move through the create/overwrite body.
|
|
328
323
|
*
|
|
329
324
|
* `meta.commandLinePending` is the value-free branch, and it is load-bearing: a `cli:`-backed recipe has no
|
|
330
325
|
* bytes yet when this runs, because the collection pass deliberately happens AFTER the confirmation this
|
|
@@ -346,8 +341,8 @@ export function decideSecretWrite(resolvedValue, exists, meta) {
|
|
|
346
341
|
if (!exists) {
|
|
347
342
|
return { kind: "skip-empty" };
|
|
348
343
|
}
|
|
349
|
-
if (meta.injectionChange !== undefined || meta.
|
|
350
|
-
return { kind: "metadata-update", ...meta.injectionChange, ...meta.
|
|
344
|
+
if (meta.injectionChange !== undefined || meta.labelsChange !== undefined) {
|
|
345
|
+
return { kind: "metadata-update", ...meta.injectionChange, ...meta.labelsChange };
|
|
351
346
|
}
|
|
352
347
|
return { kind: "noop-resolve" };
|
|
353
348
|
}
|
|
@@ -77,7 +77,7 @@ export declare function resolveWorkflow(payload: WorkflowPayload, resolvers: Wor
|
|
|
77
77
|
export declare function workflowChanges(desired: ResolvedWorkflow, live: LiveWorkflow): WorkflowFieldChange[];
|
|
78
78
|
export interface ReconcileWorkflowsOptions {
|
|
79
79
|
resolvers: WorkflowResolvers;
|
|
80
|
-
/** The factory whose
|
|
80
|
+
/** The factory whose managed-by stamp marks the entries apply pins. */
|
|
81
81
|
factory: string;
|
|
82
82
|
/** Preview only: every write is fenced. */
|
|
83
83
|
dryRun?: boolean;
|