swallowkit 2.0.0-alpha.5 → 2.0.0-alpha.7
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/dist/cli/commands/provision.d.ts +4 -1
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +233 -69
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +14 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/provision-deployment.d.ts +45 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +151 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +39 -5
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +609 -103
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +8 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +11 -3
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +30 -2
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +17 -11
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +58 -12
- package/dist/mcp/index.js.map +1 -1
- package/dist/utils/azure-cli.d.ts +16 -4
- package/dist/utils/azure-cli.d.ts.map +1 -1
- package/dist/utils/azure-cli.js +166 -4
- package/dist/utils/azure-cli.js.map +1 -1
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/agent-instructions.test.ts +5 -0
- package/src/__tests__/agent-loop-phase34.test.ts +19 -8
- package/src/__tests__/azure-cli.test.ts +5 -2
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +138 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/mcp.test.ts +56 -0
- package/src/__tests__/provision-runtime.test.ts +636 -0
- package/src/cli/commands/provision.ts +243 -70
- package/src/cli/index.ts +12 -2
- package/src/core/operations/provision-deployment.ts +167 -0
- package/src/core/operations/provision-operations.ts +597 -145
- package/src/core/project/agent-templates/AGENTS.md +22 -0
- package/src/core/project/agent-templates/CLAUDE.md +7 -0
- package/src/core/project/agent-templates/copilot-instructions.md +8 -0
- package/src/core/project/capabilities.ts +19 -3
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +30 -2
- package/src/machine/index.ts +20 -14
- package/src/mcp/index.ts +50 -15
- package/src/utils/azure-cli.ts +167 -6
- package/src/utils/azure-diagnostics.ts +75 -0
|
@@ -224,6 +224,10 @@ required.
|
|
|
224
224
|
|
|
225
225
|
Never invent or implicitly grant human approval.
|
|
226
226
|
|
|
227
|
+
Humans normally invoke `swallowkit provision` directly. Its invocation requests
|
|
228
|
+
execution without an additional confirmation. Agents must still use the
|
|
229
|
+
machine/MCP approval flow and must not switch to normal CLI to bypass it.
|
|
230
|
+
|
|
227
231
|
## Model Changes
|
|
228
232
|
|
|
229
233
|
For an existing model change, the normal pattern is:
|
|
@@ -401,6 +405,24 @@ Use the `swallowkit-provision` Agent Skill or runbook and never bypass its
|
|
|
401
405
|
approval gate. A broad request to deploy does not itself grant the explicit
|
|
402
406
|
approval required by that workflow.
|
|
403
407
|
|
|
408
|
+
Provision applies Bicep once and tracks the original Azure deployment with a
|
|
409
|
+
bounded wait. A local timeout/error, output truncation, or Ctrl+C is not proof
|
|
410
|
+
that Azure failed. `in-progress` means Azure is Running/Accepted; `unknown`
|
|
411
|
+
means its result could not be read; `partial` means Azure succeeded but
|
|
412
|
+
tag/ledger completion is pending. Never re-submit Bicep, reconcile, adopt,
|
|
413
|
+
cancel, or delete just to repair waiting or migration bookkeeping.
|
|
414
|
+
|
|
415
|
+
Retain `.swallowkit/state/provision/` execution checkpoints. Use
|
|
416
|
+
`{{runCmd}} swallowkit machine inspect provision --plan <runId> --wait-seconds 300`
|
|
417
|
+
for read-only status/wait, and after explicit human approval use
|
|
418
|
+
`{{runCmd}} swallowkit machine apply provision --plan <runId> --approve`
|
|
419
|
+
to resume. `runId` equals the original `planId`; fingerprints, subscription,
|
|
420
|
+
deployment identity and migration checks remain in force. Only
|
|
421
|
+
`status: "complete"` / `outcome: "succeeded"` means Azure Succeeded plus verified
|
|
422
|
+
tags and local ledger. Do not print deployment output secrets or raw `az --debug`.
|
|
423
|
+
For older untracked baselines use `plan provision --resume-deployment <name>`
|
|
424
|
+
with original target/regions; it blocks unless ARM/parameter evidence matches.
|
|
425
|
+
|
|
404
426
|
## SwallowKit Agent Skills and Runbooks
|
|
405
427
|
|
|
406
428
|
SwallowKit installs task-specific Agent Skills under:
|
|
@@ -24,6 +24,13 @@ Always-on rules:
|
|
|
24
24
|
after apply).
|
|
25
25
|
- Use plan/apply for deterministic generation.
|
|
26
26
|
- Respect all `requires-human` states and provisioning approval gates.
|
|
27
|
+
- Normal `swallowkit provision` is for humans. Agents must not use it to bypass
|
|
28
|
+
the machine/MCP approval flow.
|
|
29
|
+
- Provision timeouts/Ctrl+C do not prove Azure failed. Inspect the original
|
|
30
|
+
execution checkpoint with `machine inspect provision --plan <runId>`; resume
|
|
31
|
+
`machine apply provision --plan <runId> --approve` only after explicit human
|
|
32
|
+
approval. Preserve checkpoints; never re-submit Bicep/reconcile/cancel/delete
|
|
33
|
+
to repair waiting or tag/ledger failures. Do not print deployment output secrets.
|
|
27
34
|
- Run SwallowKit verification before considering implementation complete.
|
|
28
35
|
- Default verification includes `backend-build` for standalone Functions.
|
|
29
36
|
Skipped toolchains are not validation; Python compilation checks syntax only.
|
|
@@ -23,6 +23,14 @@ Key rules:
|
|
|
23
23
|
after apply).
|
|
24
24
|
- Use plan/apply for deterministic generation.
|
|
25
25
|
- Respect `requires-human` states and other human approval gates.
|
|
26
|
+
- Normal `swallowkit provision` is a human-facing deployment command. Agents must
|
|
27
|
+
use the machine/MCP approval flow, not normal CLI as an approval bypass.
|
|
28
|
+
- Provision timeouts/Ctrl+C do not prove Azure failed. Read `status`, `outcome`,
|
|
29
|
+
and `nextActions`; inspect the original run with `machine inspect provision
|
|
30
|
+
--plan <runId>`, then resume `machine apply provision --plan <runId> --approve`
|
|
31
|
+
only after explicit human approval. Preserve execution checkpoints; never
|
|
32
|
+
re-submit Bicep or use reconcile/cancel/delete to repair waiting or tag/ledger
|
|
33
|
+
failures. Do not print deployment output secrets.
|
|
26
34
|
- Run SwallowKit verification before completion.
|
|
27
35
|
- Default verification includes `backend-build` for standalone Functions.
|
|
28
36
|
Skipped toolchains are not validation; Python compilation checks syntax only.
|
|
@@ -61,6 +61,14 @@ export interface CapabilitiesContract {
|
|
|
61
61
|
notes: string[];
|
|
62
62
|
};
|
|
63
63
|
machineCommands: string[];
|
|
64
|
+
provisioning: {
|
|
65
|
+
execution: string;
|
|
66
|
+
completion: string;
|
|
67
|
+
tracking: string;
|
|
68
|
+
recovery: string;
|
|
69
|
+
diagnostics: string;
|
|
70
|
+
approval: string;
|
|
71
|
+
};
|
|
64
72
|
verifyChecks: {
|
|
65
73
|
defaultChecks: string[];
|
|
66
74
|
optInChecks: string[];
|
|
@@ -214,16 +222,24 @@ export function inspectCapabilities(): CapabilitiesContract {
|
|
|
214
222
|
notes: ["Editing dev-seeds/*.json alone does nothing; seeds are only applied via dev --seed-env."],
|
|
215
223
|
},
|
|
216
224
|
machineCommands: [
|
|
217
|
-
"inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities",
|
|
225
|
+
"inspect project | entities | routes | artifacts | boundaries | infra | drift | capabilities; inspect provision --plan <runId> [--wait-seconds <0-3600>]",
|
|
218
226
|
"validate project",
|
|
219
227
|
"generate model <names...> | generate scaffold <model>",
|
|
220
|
-
"plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if]",
|
|
221
|
-
"apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve",
|
|
228
|
+
"plan scaffold <models...> [--api-only] [--skip-infra] | plan auth --provider <p> [--scheme <name>] [--allowed-providers <csv>] | plan provision -g <rg> --location <loc> --swa-location <loc> [--adopt-existing --baseline 0 --what-if] [--reconcile --what-if] [--resume-deployment <name>]",
|
|
229
|
+
"apply scaffold [models...] --plan <id> [--approve] [--skip-infra] | apply auth --plan <id> [--approve] | apply provision --plan <id> --approve [--wait-seconds <0-3600>] (also resumes the same execution checkpoint)",
|
|
222
230
|
"verify project [--checks <ids>] [--compact]",
|
|
223
231
|
"explain failure [--check <id>]",
|
|
224
232
|
"(regular CLI) migrations add-model <model> [--dry-run] — generate only the container migration after scaffold --skip-infra",
|
|
225
233
|
"(regular CLI) configure-mcp [--version auto|latest|<semver>] — re-select the MCP launcher version in .mcp.json",
|
|
226
234
|
],
|
|
235
|
+
provisioning: {
|
|
236
|
+
execution: "Submit Bicep once with az deployment group create --no-wait, then bounded state polling (default 300 seconds, configurable 0-3600). Azure CLI invocations are bounded separately. Warning-only stderr with exit 0 is not a failure; output capture limits do not terminate az.",
|
|
237
|
+
completion: "status complete/outcome succeeded requires Azure Succeeded plus verified migration tags and local ledger completion. Running/Accepted -> in-progress; unreadable/not-found state -> blocked/outcome unknown; bookkeeping interruption after Azure Succeeded -> blocked/outcome partial; Failed/Canceled -> failed with Azure causes.",
|
|
238
|
+
tracking: "Approved runs persist before Azure writes under .swallowkit/state/provision/. runId equals the original planId and preserves the subscription, RG, deployment ID/correlation, fingerprints and migration state. inspect provision is read-only; apply provision --plan resumes without re-submitting attempted deployments. Ctrl+C/termination may leave Azure running; never cancel/delete without explicit consent.",
|
|
239
|
+
recovery: "For an older untracked baseline, plan provision --resume-deployment <name> compares the existing ARM template and non-secure parameters to the local Bicep/parameters. It observes that deployment and only tags/records it after Succeeded. Mismatches, unreadable evidence, secure parameters or non-baseline migrations block; do not use reconcile just to repair bookkeeping.",
|
|
240
|
+
diagnostics: "Machine/MCP results include bounded/redacted stdout/stderr, omitted-output flags, exit code, signal, failure kind, elapsed time and deployment identity, but no deployment outputs. Normal CLI shows named resource information and CI/CD setup guidance after success. Never print --debug output, keys, tokens or connection strings.",
|
|
241
|
+
approval: "Machine/MCP initial apply and every resume require explicit human approval. A human invoking normal provision or provision --resume explicitly requests execution without another confirmation prompt. Agents must not switch to normal CLI to bypass approval. A passing local command or an ok envelope is not proof of completed Azure provisioning; inspect status, outcome and nextActions.",
|
|
242
|
+
},
|
|
227
243
|
verifyChecks: {
|
|
228
244
|
defaultChecks: ["structure", "drift", "typecheck", "backend-build"],
|
|
229
245
|
optInChecks: ["build", "lint", "test"],
|
|
@@ -57,6 +57,76 @@ export function deletePlanState(planId: string, projectRoot: string = process.cw
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function provisionRunPath(runId: string, projectRoot: string): string {
|
|
61
|
+
if (!/^[a-f0-9]{12}$/.test(runId)) throw new Error("Invalid provision run ID.");
|
|
62
|
+
return path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision", `${runId}.json`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function saveProvisionRun<T extends { runId: string }>(run: T, projectRoot: string = process.cwd()): void {
|
|
66
|
+
ensureStateDir(projectRoot);
|
|
67
|
+
const file = provisionRunPath(run.runId, projectRoot);
|
|
68
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
69
|
+
const temporary = `${file}.tmp-${process.pid}`;
|
|
70
|
+
fs.writeFileSync(temporary, `${JSON.stringify(run, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
71
|
+
fs.renameSync(temporary, file);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function loadProvisionRun<T>(runId: string, projectRoot: string = process.cwd()): T | null {
|
|
75
|
+
const file = provisionRunPath(runId, projectRoot);
|
|
76
|
+
if (!fs.existsSync(file)) return null;
|
|
77
|
+
// Unlike a preview, discarding a corrupt execution checkpoint could replay Azure writes.
|
|
78
|
+
return JSON.parse(fs.readFileSync(file, "utf-8")) as T;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function listProvisionRuns<T>(projectRoot: string = process.cwd()): T[] {
|
|
82
|
+
const directory = path.join(projectRoot, SWALLOWKIT_STATE_DIR, "provision");
|
|
83
|
+
if (!fs.existsSync(directory)) return [];
|
|
84
|
+
return fs.readdirSync(directory).filter((name) => /^[a-f0-9]{12}\.json$/.test(name))
|
|
85
|
+
.map((name) => loadProvisionRun<T>(name.slice(0, -5), projectRoot)!);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A live local executor must not share an Azure target with another executor. */
|
|
89
|
+
export function lockProvisionTarget(key: string, projectRoot: string = process.cwd()): () => void {
|
|
90
|
+
if (!/^[a-f0-9]{64}$/.test(key)) throw new Error("Invalid provision lock key.");
|
|
91
|
+
const directory = path.join(ensureStateDir(projectRoot), "provision");
|
|
92
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
93
|
+
const file = path.join(directory, `${key}.lock`);
|
|
94
|
+
const create = () => {
|
|
95
|
+
const descriptor = fs.openSync(file, "wx", 0o600);
|
|
96
|
+
try { fs.writeFileSync(descriptor, String(process.pid)); } finally { fs.closeSync(descriptor); }
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
create();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
102
|
+
const recoveryFile = `${file}.recovery`;
|
|
103
|
+
let descriptor: number;
|
|
104
|
+
try {
|
|
105
|
+
descriptor = fs.openSync(recoveryFile, "wx", 0o600);
|
|
106
|
+
} catch (recoveryError) {
|
|
107
|
+
if ((recoveryError as NodeJS.ErrnoException).code !== "EEXIST") throw recoveryError;
|
|
108
|
+
throw new Error("Provision lock recovery is already in progress. Inspect the checkpoint before retrying.");
|
|
109
|
+
}
|
|
110
|
+
fs.closeSync(descriptor);
|
|
111
|
+
try {
|
|
112
|
+
const pid = Number(fs.readFileSync(file, "utf-8"));
|
|
113
|
+
if (!Number.isInteger(pid) || pid < 1) throw new Error("Invalid provision lock; inspect the checkpoint before removing the lock.");
|
|
114
|
+
try {
|
|
115
|
+
process.kill(pid, 0);
|
|
116
|
+
} catch (probeError) {
|
|
117
|
+
if ((probeError as NodeJS.ErrnoException).code !== "ESRCH") throw probeError;
|
|
118
|
+
fs.unlinkSync(file);
|
|
119
|
+
create();
|
|
120
|
+
return () => fs.unlinkSync(file);
|
|
121
|
+
}
|
|
122
|
+
throw new Error("Another local provision executor is using this Azure target. Inspect its run; do not start a second deployment.");
|
|
123
|
+
} finally {
|
|
124
|
+
fs.unlinkSync(recoveryFile);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return () => fs.unlinkSync(file);
|
|
128
|
+
}
|
|
129
|
+
|
|
60
130
|
const LAST_VERIFY_FILE = "last-verify.json";
|
|
61
131
|
|
|
62
132
|
export function saveLastVerifyState(result: unknown, projectRoot: string = process.cwd()): void {
|
|
@@ -211,9 +211,14 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
|
|
|
211
211
|
\`status: "requires-human"\` and \`requiresApproval: true\` by design.
|
|
212
212
|
- Never pass \`--approve\` to \`apply provision\` unless a human explicitly
|
|
213
213
|
instructed you to provision in this session.
|
|
214
|
+
- This plan/apply workflow is for agents. Humans normally run \`swallowkit provision\`
|
|
215
|
+
directly; do not switch to that command to bypass the agent approval gate.
|
|
214
216
|
- Inspect infra locally first — no \`az\` calls are needed for inspection.
|
|
215
217
|
- Never edit an applied migration. Create a corrective migration instead.
|
|
216
218
|
- Never use \`--reconcile\` for an ordinary model or resource addition.
|
|
219
|
+
- A local az error/timeout or Ctrl+C does not prove Azure failed. Never
|
|
220
|
+
re-submit, cancel, or delete a deployment to repair a wait or bookkeeping failure.
|
|
221
|
+
- Respect \`status\`, \`outcome\`, and \`nextActions\`, not just \`ok\`.
|
|
217
222
|
|
|
218
223
|
## Steps
|
|
219
224
|
|
|
@@ -221,7 +226,7 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
|
|
|
221
226
|
\`${runCmd} swallowkit machine inspect infra\`
|
|
222
227
|
- review parameters, modules, outputs, and container wiring warnings.
|
|
223
228
|
2. Compute the provisioning plan (no resources are created):
|
|
224
|
-
\`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region>\`
|
|
229
|
+
\`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region> --swa-location <swa-region>\`
|
|
225
230
|
- the plan reports \`bootstrap\`, \`migrate\`, \`noop\`, or \`adoption-required\`.
|
|
226
231
|
- it lists only pending migrations and the exact \`az\` commands that would run.
|
|
227
232
|
- for an older untagged environment, first run \`${runCmd} swallowkit migrations init --legacy-baseline\`, then plan with \`--adopt-existing --baseline 0 --what-if\`.
|
|
@@ -229,12 +234,35 @@ Goal: bootstrap Azure once, then apply only pending versioned infrastructure mig
|
|
|
229
234
|
3. Present the plan to the human and wait for explicit approval.
|
|
230
235
|
4. Only after approval:
|
|
231
236
|
\`${runCmd} swallowkit machine apply provision --plan <planId> --approve\`
|
|
232
|
-
5.
|
|
237
|
+
5. If \`in-progress\`, \`unknown\`, or \`partial\`, retain the execution
|
|
238
|
+
checkpoint and read/wait for the original deployment:
|
|
239
|
+
\`${runCmd} swallowkit machine inspect provision --plan <runId> --wait-seconds 300\`.
|
|
240
|
+
Inspection never writes Azure tags or the local ledger.
|
|
241
|
+
6. After fresh explicit human approval, resume:
|
|
242
|
+
\`${runCmd} swallowkit machine apply provision --plan <runId> --approve\`.
|
|
243
|
+
Attempted deployments are not re-submitted; remaining reviewed steps and
|
|
244
|
+
tags/ledger finish only after Azure \`Succeeded\`.
|
|
245
|
+
7. Report completion only when \`status: "complete"\` and
|
|
246
|
+
\`outcome: "succeeded"\`. Never report deployment output secrets.
|
|
247
|
+
|
|
248
|
+
For an older untracked bootstrap, plan with \`--resume-deployment <name>\`
|
|
249
|
+
and the original RG, subscription and regions. Recovery compares its ARM
|
|
250
|
+
template and non-secure parameters to local Bicep before approval. Mismatch,
|
|
251
|
+
unreadable evidence or secure parameters block; do not work around this by
|
|
252
|
+
using reconcile/adoption or replacing the migration manifest.
|
|
233
253
|
|
|
234
254
|
## Terminal states
|
|
235
255
|
|
|
236
256
|
\`apply provision\` without \`--approve\` fails with \`approval-required\` —
|
|
237
257
|
this is the guardrail working as intended, not an error to work around.
|
|
258
|
+
|
|
259
|
+
Azure \`Running\`/\`Accepted\` means \`in-progress\`. An unreadable or missing
|
|
260
|
+
deployment means \`unknown\`, not Azure failure. Azure \`Failed\`/\`Canceled\`
|
|
261
|
+
returns sanitized Azure and operation causes. Azure \`Succeeded\` with incomplete
|
|
262
|
+
tag/ledger writes means \`partial\`; resume bookkeeping without redeploying.
|
|
263
|
+
The default wait is 300 seconds (0-3600 via \`--wait-seconds\`), not unlimited.
|
|
264
|
+
Warning-only stderr with exit 0 is accepted. Diagnostics report local exit,
|
|
265
|
+
signal, elapsed time and truncated/redacted output separately from Azure state.
|
|
238
266
|
`,
|
|
239
267
|
};
|
|
240
268
|
|
package/src/machine/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { isSwallowKitProject } from "../core/config";
|
|
|
3
3
|
import { createModelOperation } from "../core/operations/create-model";
|
|
4
4
|
import { applyScaffoldOperation } from "../core/operations/scaffold-apply";
|
|
5
5
|
import { applyAuthOperation, planAuthOperation } from "../core/operations/auth-operations";
|
|
6
|
-
import { applyProvisionOperation, planProvisionOperation } from "../core/operations/provision-operations";
|
|
6
|
+
import { applyProvisionOperation, inspectProvisionOperation, planProvisionOperation } from "../core/operations/provision-operations";
|
|
7
7
|
import { ProcessExitInterceptError } from "../core/operations/runtime";
|
|
8
8
|
import { inspectInfra } from "../core/project/infra";
|
|
9
9
|
import { runMachineScaffoldOperation } from "../core/operations/scaffold-machine";
|
|
@@ -141,6 +141,17 @@ function createMachineProgram(): Command {
|
|
|
141
141
|
.showHelpAfterError(false);
|
|
142
142
|
|
|
143
143
|
const inspect = new Command("inspect");
|
|
144
|
+
inspect
|
|
145
|
+
.command("provision")
|
|
146
|
+
.description("Read/wait for an original provision deployment without writes or replay")
|
|
147
|
+
.requiredOption("--plan <runId>", "Original provision plan/execution checkpoint ID")
|
|
148
|
+
.option("--wait-seconds <seconds>", "Bounded read-only wait, 0-3600 seconds (default: 0)", Number)
|
|
149
|
+
.action(async (options: { plan: string; waitSeconds?: number }) => {
|
|
150
|
+
await handleMachineAction("inspect-provision", async () => {
|
|
151
|
+
ensureProjectForMachine();
|
|
152
|
+
return inspectProvisionOperation({ planId: options.plan, waitSeconds: options.waitSeconds });
|
|
153
|
+
}, (result) => ({ status: result.status, nextActions: result.nextActions }));
|
|
154
|
+
});
|
|
144
155
|
inspect
|
|
145
156
|
.command("project")
|
|
146
157
|
.description("Inspect SwallowKit project metadata")
|
|
@@ -373,7 +384,7 @@ function createMachineProgram(): Command {
|
|
|
373
384
|
|
|
374
385
|
plan
|
|
375
386
|
.command("provision")
|
|
376
|
-
.description("
|
|
387
|
+
.description("Review local infrastructure and current Azure state without writes (requires az login)")
|
|
377
388
|
.requiredOption("-g, --resource-group <name>", "Resource group name")
|
|
378
389
|
.requiredOption("--location <region>", "Primary location for Functions and Cosmos DB")
|
|
379
390
|
.requiredOption("--swa-location <region>", "Static Web App location")
|
|
@@ -382,7 +393,8 @@ function createMachineProgram(): Command {
|
|
|
382
393
|
.option("--adopt-existing", "Adopt an existing untagged resource group without deploying main.bicep", false)
|
|
383
394
|
.option("--baseline <version>", "Baseline version for adoption", (value) => Number(value))
|
|
384
395
|
.option("--reconcile", "Explicitly redeploy infra/main.bicep", false)
|
|
385
|
-
.
|
|
396
|
+
.option("--resume-deployment <name>", "Recover a matching older baseline deployment without re-submitting Bicep")
|
|
397
|
+
.action(async (options: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean; resumeDeployment?: string }) => {
|
|
386
398
|
await handleMachineAction(
|
|
387
399
|
"plan-provision",
|
|
388
400
|
async () => {
|
|
@@ -396,6 +408,7 @@ function createMachineProgram(): Command {
|
|
|
396
408
|
adoptExisting: options.adoptExisting,
|
|
397
409
|
baseline: options.baseline,
|
|
398
410
|
reconcile: options.reconcile,
|
|
411
|
+
resumeDeployment: options.resumeDeployment,
|
|
399
412
|
});
|
|
400
413
|
},
|
|
401
414
|
(planData) => ({
|
|
@@ -546,22 +559,15 @@ function createMachineProgram(): Command {
|
|
|
546
559
|
.description("Apply an approved provisioning plan (always requires --approve; creates billable Azure resources)")
|
|
547
560
|
.requiredOption("--plan <planId>", "Provision plan to apply")
|
|
548
561
|
.option("--approve", "Confirm human approval of the provisioning plan", false)
|
|
549
|
-
.
|
|
562
|
+
.option("--wait-seconds <seconds>", "Bounded state wait, 0-3600 seconds (default: 300)", Number)
|
|
563
|
+
.action(async (options: { plan: string; approve?: boolean; waitSeconds?: number }) => {
|
|
550
564
|
await handleMachineAction(
|
|
551
565
|
"apply-provision",
|
|
552
566
|
async () => {
|
|
553
567
|
ensureProjectForMachine();
|
|
554
|
-
return applyProvisionOperation({ planId: options.plan, approve: options.approve });
|
|
568
|
+
return applyProvisionOperation({ planId: options.plan, approve: options.approve, waitSeconds: options.waitSeconds });
|
|
555
569
|
},
|
|
556
|
-
() => ({
|
|
557
|
-
status: "complete",
|
|
558
|
-
nextActions: [
|
|
559
|
-
{
|
|
560
|
-
command: "swallowkit machine inspect infra",
|
|
561
|
-
description: "Re-inspect infra assets after provisioning.",
|
|
562
|
-
},
|
|
563
|
-
],
|
|
564
|
-
})
|
|
570
|
+
(result) => ({ status: result.status, nextActions: result.nextActions })
|
|
565
571
|
);
|
|
566
572
|
});
|
|
567
573
|
|
package/src/mcp/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as z from "zod/v4";
|
|
|
8
8
|
import type { MachineErrorResponse, MachineNextAction, MachineOperationStatus, MachineResponse } from "../machine/contracts";
|
|
9
9
|
import { getSwallowKitVersion } from "../version";
|
|
10
10
|
import { detectFromProject, getCommands } from "../utils/package-manager";
|
|
11
|
+
import { provisionNextActions } from "../core/operations/provision-operations";
|
|
11
12
|
import {
|
|
12
13
|
MCP_DELEGATED_FROM_ENV,
|
|
13
14
|
McpVersionContext,
|
|
@@ -24,7 +25,7 @@ interface MachineSuccessPayload<TData> {
|
|
|
24
25
|
data: TData;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
type MachineCliRunner = (args: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
28
|
+
type MachineCliRunner = (args: string[], signal?: AbortSignal) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
28
29
|
type ToolContentResult = { content: Array<{ type: "text"; text: string }>; isError?: boolean };
|
|
29
30
|
class MachineCliError extends Error {
|
|
30
31
|
constructor(message: string, readonly response: MachineErrorResponse) {
|
|
@@ -35,7 +36,7 @@ type ToolDefinition = {
|
|
|
35
36
|
name: string;
|
|
36
37
|
description: string;
|
|
37
38
|
inputSchema: z.ZodTypeAny;
|
|
38
|
-
handler: (input: any) => Promise<ToolContentResult>;
|
|
39
|
+
handler: (input: any, signal?: AbortSignal) => Promise<ToolContentResult>;
|
|
39
40
|
};
|
|
40
41
|
type ExecaModule = typeof import("execa");
|
|
41
42
|
|
|
@@ -48,29 +49,45 @@ function resolveMachineCliEntrypoint(): string {
|
|
|
48
49
|
return path.resolve(__dirname, "..", "cli", "index.js");
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
async function defaultMachineCliRunner(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
52
|
+
async function defaultMachineCliRunner(args: string[], signal?: AbortSignal): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
52
53
|
const { execa } = await importModule("execa");
|
|
53
|
-
const
|
|
54
|
+
const child = execa(process.execPath, [resolveMachineCliEntrypoint(), "machine", ...args], {
|
|
54
55
|
reject: false,
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
+
const abort = () => child.kill("SIGTERM", { forceKillAfterTimeout: 6_000 });
|
|
58
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
59
|
+
if (signal?.aborted) abort();
|
|
60
|
+
let result: Awaited<typeof child>;
|
|
61
|
+
try { result = await child; } finally { signal?.removeEventListener("abort", abort); }
|
|
57
62
|
return {
|
|
58
63
|
stdout: result.stdout,
|
|
59
64
|
stderr: result.stderr,
|
|
60
|
-
exitCode: result.exitCode ??
|
|
65
|
+
exitCode: result.exitCode ?? 1,
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
async function executeMachineCommand<TData>(
|
|
65
70
|
args: string[],
|
|
66
|
-
runMachineCli: MachineCliRunner
|
|
71
|
+
runMachineCli: MachineCliRunner,
|
|
72
|
+
signal?: AbortSignal
|
|
67
73
|
): Promise<MachineSuccessPayload<TData>> {
|
|
68
|
-
const
|
|
74
|
+
const interrupted = () => {
|
|
75
|
+
const runId = args[args.indexOf("--plan") + 1];
|
|
76
|
+
const response: MachineErrorResponse = {
|
|
77
|
+
ok: false, command: args.slice(0, 2).join("-"), status: "blocked",
|
|
78
|
+
error: { code: "provision-interrupted", message: "The local MCP wait was interrupted. Azure may continue; inspect the original checkpoint. No Azure cancellation or successful result is implied.", details: { runId, outcome: "unknown", localFailure: "cancelled" } },
|
|
79
|
+
nextActions: /^[a-f0-9]{12}$/.test(runId) ? provisionNextActions(runId) : [],
|
|
80
|
+
};
|
|
81
|
+
return new MachineCliError(response.error.message, response);
|
|
82
|
+
};
|
|
83
|
+
if (signal?.aborted) throw interrupted();
|
|
84
|
+
const result = await (signal ? runMachineCli(args, signal) : runMachineCli(args));
|
|
69
85
|
|
|
70
86
|
let parsed: MachineResponse<TData>;
|
|
71
87
|
try {
|
|
72
88
|
parsed = JSON.parse(result.stdout) as MachineResponse<TData>;
|
|
73
89
|
} catch {
|
|
90
|
+
if (signal?.aborted) throw interrupted();
|
|
74
91
|
throw new Error(result.stderr || result.stdout || "Machine CLI returned invalid JSON.");
|
|
75
92
|
}
|
|
76
93
|
|
|
@@ -423,14 +440,16 @@ export function buildSwallowKitToolDefinitions(
|
|
|
423
440
|
adoptExisting: z.boolean().optional(),
|
|
424
441
|
baseline: z.number().int().nonnegative().optional(),
|
|
425
442
|
reconcile: z.boolean().optional(),
|
|
443
|
+
resumeDeployment: z.string().optional(),
|
|
426
444
|
}),
|
|
427
|
-
handler: async ({ resourceGroup, location, swaLocation, subscription, whatIf, adoptExisting, baseline, reconcile }: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean }) => {
|
|
445
|
+
handler: async ({ resourceGroup, location, swaLocation, subscription, whatIf, adoptExisting, baseline, reconcile, resumeDeployment }: { resourceGroup: string; location: string; swaLocation: string; subscription?: string; whatIf?: boolean; adoptExisting?: boolean; baseline?: number; reconcile?: boolean; resumeDeployment?: string }) => {
|
|
428
446
|
const args = ["plan", "provision", "--resource-group", resourceGroup, "--location", location, "--swa-location", swaLocation];
|
|
429
447
|
if (subscription) args.push("--subscription", subscription);
|
|
430
448
|
if (whatIf) args.push("--what-if");
|
|
431
449
|
if (adoptExisting) args.push("--adopt-existing");
|
|
432
450
|
if (baseline !== undefined) args.push("--baseline", String(baseline));
|
|
433
451
|
if (reconcile) args.push("--reconcile");
|
|
452
|
+
if (resumeDeployment) args.push("--resume-deployment", resumeDeployment);
|
|
434
453
|
|
|
435
454
|
const response = await executeMachineCommand(args, runMachineCli);
|
|
436
455
|
return jsonTextContent(withOperationStatus(response));
|
|
@@ -438,16 +457,32 @@ export function buildSwallowKitToolDefinitions(
|
|
|
438
457
|
},
|
|
439
458
|
{
|
|
440
459
|
name: "swallowkit_apply_provision",
|
|
441
|
-
description: "Apply an approved
|
|
460
|
+
description: "Apply or resume an approved provision checkpoint with bounded state polling. Never resubmit an attempted deployment; finish tags/ledger only after Azure Succeeded. Check status/nextActions, not just ok. approve=true always requires explicit human consent.",
|
|
442
461
|
inputSchema: z.object({
|
|
443
462
|
planId: z.string(),
|
|
444
463
|
approve: z.boolean().optional(),
|
|
464
|
+
waitSeconds: z.number().int().min(0).max(3600).optional(),
|
|
445
465
|
}),
|
|
446
|
-
handler: async ({ planId, approve }: { planId: string; approve?: boolean }) => {
|
|
466
|
+
handler: async ({ planId, approve, waitSeconds }: { planId: string; approve?: boolean; waitSeconds?: number }, signal?: AbortSignal) => {
|
|
447
467
|
const args = ["apply", "provision", "--plan", planId];
|
|
448
468
|
if (approve) args.push("--approve");
|
|
469
|
+
if (waitSeconds !== undefined) args.push("--wait-seconds", String(waitSeconds));
|
|
449
470
|
|
|
450
|
-
const response = await executeMachineCommand(args, runMachineCli);
|
|
471
|
+
const response = await executeMachineCommand(args, runMachineCli, signal);
|
|
472
|
+
return jsonTextContent(withOperationStatus(response));
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "swallowkit_inspect_provision",
|
|
477
|
+
description: "Read or bounded-wait for the original provision deployment. No create, cancel, delete, tag, or ledger writes. Azure Succeeded alone is partial until the original approved checkpoint finishes bookkeeping.",
|
|
478
|
+
inputSchema: z.object({
|
|
479
|
+
planId: z.string(),
|
|
480
|
+
waitSeconds: z.number().int().min(0).max(3600).optional(),
|
|
481
|
+
}),
|
|
482
|
+
handler: async ({ planId, waitSeconds }: { planId: string; waitSeconds?: number }, signal?: AbortSignal) => {
|
|
483
|
+
const args = ["inspect", "provision", "--plan", planId];
|
|
484
|
+
if (waitSeconds !== undefined) args.push("--wait-seconds", String(waitSeconds));
|
|
485
|
+
const response = await executeMachineCommand(args, runMachineCli, signal);
|
|
451
486
|
return jsonTextContent(withOperationStatus(response));
|
|
452
487
|
},
|
|
453
488
|
},
|
|
@@ -456,9 +491,9 @@ export function buildSwallowKitToolDefinitions(
|
|
|
456
491
|
// Every response reports which SwallowKit version answered and why it was selected.
|
|
457
492
|
return definitions.map((definition) => ({
|
|
458
493
|
...definition,
|
|
459
|
-
handler: async (input: unknown) => {
|
|
494
|
+
handler: async (input: unknown, signal?: AbortSignal) => {
|
|
460
495
|
try {
|
|
461
|
-
const result = await definition.handler(input);
|
|
496
|
+
const result = await definition.handler(input, signal);
|
|
462
497
|
return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
|
|
463
498
|
} catch (error) {
|
|
464
499
|
// Prerequisite failures carry nextActions; keep them structured so agents can follow them.
|
|
@@ -488,7 +523,7 @@ export function createSwallowKitMcpServer(
|
|
|
488
523
|
description: tool.description,
|
|
489
524
|
inputSchema: tool.inputSchema,
|
|
490
525
|
},
|
|
491
|
-
tool.handler
|
|
526
|
+
(input, extra) => tool.handler(input, extra.signal)
|
|
492
527
|
);
|
|
493
528
|
}
|
|
494
529
|
|