@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.
Files changed (94) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/api/approval.js +3 -3
  3. package/dist/cli/commands/outpost/local.js +3 -3
  4. package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
  5. package/dist/cli/commands/outpost/machine-names.js +22 -0
  6. package/dist/cli/commands/outpost/up.js +7 -5
  7. package/dist/cli/commands/worker/enroll.d.ts +4 -3
  8. package/dist/cli/commands/worker/enroll.js +4 -3
  9. package/dist/cli/core/auth.d.ts +22 -2
  10. package/dist/cli/core/auth.js +69 -8
  11. package/dist/cli/core/errors.js +4 -0
  12. package/dist/cli/core/platform-client.d.ts +3 -2
  13. package/dist/cli/core/session-body.d.ts +1 -1
  14. package/dist/cli/core/session-body.js +1 -1
  15. package/dist/cli/firstRun.d.ts +3 -3
  16. package/dist/cli/firstRun.js +7 -5
  17. package/dist/cli/main.js +4 -3
  18. package/dist/constructs/environment.d.ts +24 -11
  19. package/dist/constructs/environment.js +76 -12
  20. package/dist/constructs/secret.d.ts +17 -7
  21. package/dist/constructs/secret.js +24 -16
  22. package/dist/core/labels.d.ts +32 -6
  23. package/dist/core/labels.js +70 -8
  24. package/dist/engine/diff.js +38 -11
  25. package/dist/engine/gateway-key-reconcile.d.ts +1 -2
  26. package/dist/engine/gateway-key-reconcile.js +19 -15
  27. package/dist/engine/pipeline-reconcile.js +2 -2
  28. package/dist/engine/pipeline-workflow-reconcile.js +15 -9
  29. package/dist/engine/reconcile.js +48 -15
  30. package/dist/engine/refusals.d.ts +0 -5
  31. package/dist/engine/refusals.js +1 -5
  32. package/dist/engine/secrets-apply.d.ts +10 -14
  33. package/dist/engine/secrets-apply.js +12 -17
  34. package/dist/engine/workflow-reconcile.d.ts +1 -1
  35. package/dist/generated/cli-commands.json +170 -34
  36. package/dist/generated/providers.d.ts +40 -0
  37. package/dist/generated/providers.js +30 -0
  38. package/docs/REFERENCE.md +26 -12
  39. package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
  40. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
  41. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
  42. package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
  43. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
  44. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
  45. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
  46. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
  47. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
  48. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
  49. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
  50. package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
  51. package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
  52. package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
  53. package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
  54. package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
  55. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
  56. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
  57. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
  58. package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
  59. package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
  60. package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
  61. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
  62. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
  63. package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
  64. package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
  65. package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
  66. package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
  67. package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
  68. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
  69. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
  70. package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
  71. package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
  72. package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
  73. package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
  74. package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
  75. package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
  76. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
  77. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
  78. package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
  79. package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
  80. package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
  81. package/node_modules/@orchestraworks/client/package.json +1 -1
  82. package/package.json +4 -4
  83. package/skills/orchestra/SKILL.md +38 -17
  84. package/skills/orchestra-fdk/references/authoring.md +34 -26
  85. package/skills/orchestra-fdk/references/environment.md +49 -12
  86. package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
  87. package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
  88. package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
  89. package/skills/orchestra-fdk/references/troubleshooting.md +5 -3
  90. package/templates/bug-fix/main.ts +7 -4
  91. package/templates/engineering-team/lib/environment.ts +6 -3
  92. package/templates/engineering-team/package.json +1 -1
  93. package/templates/todo-factory/main.ts +7 -4
  94. 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 { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, refuseIf } from "./refusals.js";
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 { declaredSecretHosts, metadataHostsChange, metadataInjectionChange, pendingSecretWrite, reconcileInjection, } from "./secrets-apply.js";
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 "./refusals.js";
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 or hosts edit still reconciles
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 hostsChange = metadataHostsChange(declaredSecretHosts(decision.secretResource.payload), liveSecret.hosts);
156
+ const labelsMove = oldMarkersMoved(liveSecret.labels);
157
157
  let secretAction = "unchanged";
158
- if (injectionChange !== undefined || hostsChange !== undefined) {
159
- const body = { ...injectionChange, ...hostsChange };
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 = { [MANAGED_BY_LABEL_KEY]: factory, [PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: null };
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?.[PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY] !== undefined;
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.injection;
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, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
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[MANAGED_BY_LABEL_KEY] !== factory && refs.has(entry.workflow.id))
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 `sfdk.managedBy`:
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[MANAGED_BY_LABEL_KEY] === factory);
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[MANAGED_BY_LABEL_KEY] !== factory),
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[MANAGED_BY_LABEL_KEY] === factory && entry.workflow.id === workflow.id && entry.version === undefined);
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
- /** The fields a patch must send to make [live] the declared entry, null resetting; `undefined` when none. */
169
- function patchOf(entry, live) {
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);
@@ -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, declaredSecretHosts, decideSecretWrite, metadataHostsChange, metadataInjectionChange, pendingSecretWrite, reconcileInjection, replacesLiveCredentialValue, resolveSecretValueSource, } from "./secrets-apply.js";
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 desiredInjection = resource.payload.injection;
380
- const injectionChange = metadataInjectionChange(desiredInjection, live?.injection);
381
- const hostsChange = metadataHostsChange(declaredSecretHosts(resource.payload), live?.hosts);
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
- hostsChange,
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.injection;
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
- ...(desiredHosts !== undefined ? { hosts: desiredHosts } : {}),
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 hosts changed: a value-less PATCH carrying
522
- // only the changed fields.
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.hosts !== undefined ? { hosts: p.decision.hosts } : {}),
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
- async function applyEnvironment(resource, apis, outcomes, applied, reserved, teams) {
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;
@@ -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
- * The hosts delta for the value-less PATCH path, compared EXACTLY rather than as a subset: a live row at `[]`
133
- * after the platform added the column must take a declared list, and a declared `[]` must clear a live one.
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 metadataHostsChange(desired: readonly string[] | undefined, live: readonly string[] | undefined): {
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 or hosts edit changes no secret VALUE, so it must not be dropped as a noop.
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
- hosts?: string[];
158
+ labels?: Record<string, string | null>;
163
159
  } | {
164
160
  kind: "skip-empty";
165
161
  };
166
162
  /**
167
- * `meta.injectionChange` and `meta.hostsChange` steer only the no-value-plus-exists branch, since a fresh
168
- * value already writes the recipe and the hosts through the create/overwrite body.
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
- hostsChange?: {
183
- hosts: string[];
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
- * The hosts delta for the value-less PATCH path, compared EXACTLY rather than as a subset: a live row at `[]`
310
- * after the platform added the column must take a declared list, and a declared `[]` must clear a live one.
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 metadataHostsChange(desired, live) {
314
- if (desired === undefined)
312
+ export function declaredSecretInjection(payload) {
313
+ const { injection, hosts } = payload;
314
+ if (injection === undefined)
315
315
  return undefined;
316
- const current = live ?? [];
317
- const same = desired.length === current.length && desired.every((host, index) => host === current[index]);
318
- return same ? undefined : { hosts: [...desired] };
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.hostsChange` steer only the no-value-plus-exists branch, since a fresh
327
- * value already writes the recipe and the hosts through the create/overwrite body.
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.hostsChange !== undefined) {
350
- return { kind: "metadata-update", ...meta.injectionChange, ...meta.hostsChange };
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 `sfdk.managedBy` stamp marks the entries apply pins. */
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;