diff --git a/CLAUDE.md b/CLAUDE.md index 7daf22da..915603a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,6 +63,8 @@ make sync-platform-specs-from-sdk JAMFPLATFORM_SDK_REF=v0.20.1 # a tag or full S make sync-platform-specs-from-sdk JAMFPLATFORM_SDK_PATH=/path/to/jamfplatform-go-sdk ``` +**The spec ingest does not touch `go.mod`, and the two drifted six minor versions before anyone noticed.** `make sync-platform-specs-from-sdk` copies `api/` and re-derives the manifests; the SDK is *also* a Go dependency (the platform client, its transport and its retry policy), and nothing bumps it. At the v1.0.0 ingest `go.mod` still required **v0.20.1** while the committed specs had come from v0.22.2 — harmless there, because the CLI drives the gateway through the SDK's transport rather than its generated methods, so a spec ahead of the library costs nothing until a hand-written call site wants a method the pinned version lacks. Still: an ingest is two steps, `go get github.com/Jamf-Concepts/jamfplatform-go-sdk@ && go mod tidy` after the sync, and the same tag for both so the recorded `sdkCommit` and the linked library agree. + `scripts/fetch-sdk-specs.sh` does the fetching, and the local checkout stays supported for the cases the repo cannot serve: an unpublished branch, a worktree pinned to one revision, an offline machine. **Both routes end in the same two things** — the files in `specs/.platform-source/` and one recorded revision — so only the fetch differs and the derivation is shared. Verified by running the remote route at `v0.20.1` over a tree ingested from a local checkout of the same tag: `git status` came back clean, byte for byte. Two details in the script are load-bearing. It **resolves the ref to a commit SHA and downloads at that SHA**, never at the ref, because a branch can move between the resolution and the last download — which would assemble a drop from two revisions and stamp it with neither, surfacing as a spec mismatch rather than an error. And it seeds its `curl` argument array with the flags every request wants rather than leaving it empty, because macOS ships **bash 3.2**, where `"${a[@]}"` on an empty array is an unbound-variable error under `set -u`; the symptom was an unrelated JSON parse traceback from the pipeline downstream, blaming the GitHub response for a shell fault. `JAMFPLATFORM_SDK_REV` is how the resolved revision reaches `sync-gateway-coverage`, which has no checkout to run its `git rev-parse` probe against on that route. @@ -449,11 +451,11 @@ These are generated by the **Platform** generator (`generator/platform/`) into ` Because the two halves take **different credentials**, every command says which API serves it. The `Short` carries it in words — `(Security Cloud · platform gateway)` vs `(Security Cloud · Radar API)` — which is what `security ` shows, since Cobra uses `Short` as the shell-completion description. The `jamf:api` annotation (`platform-gateway` / `radar`) carries the same fact for machines and surfaces in the `commands` catalog as `api`. Both come from the generators, so a new resource gets them without a CLI edit; `TestSecurityCommandsDeclareTheirAPI` fails if one is emitted without the other. `security setup` declares neither — it writes credentials and calls no API. -**Specs are sourced from the SDK, not from `public-apis-oas`.** Drop `jamfplatform-go-sdk`'s published `api/securitycloud_*.json` into `specs/.platform-source/`, then `make sync-platform-specs`. The upstream `-beta` specs are wrong in ways only wire probing revealed, and the corrections live in the SDK's `tools/generate/config.json`, validated by acceptance tests against a live tenant. The SDK publishes them as OpenAPI extensions that this generator reads, so no override table is duplicated here — `specs/platform/` is a mixed drop, and the mix narrowed sharply at the v0.22.0 ingest (neither v0.22.1 nor v0.22.2 moved a platform spec at all — both land entirely inside the Classic body schemas; see the repeated-element note): **twelve of the seventeen specs are now v2082** — blueprints, compliance-benchmarks, declaration-reporting, both device-inventory specs, device-management-action, and all six Security Cloud specs (categories, device-groups, dns, enrollment, uem-connect, ztna). v2082 is the scope-declaration build, and it moved *every operation in every one of those twelve* by rewriting one thing (see the scope migration below), which is why the diff looks alarming and is not. +**Specs are sourced from the SDK, not from `public-apis-oas`.** Drop `jamfplatform-go-sdk`'s published `api/securitycloud_*.json` into `specs/.platform-source/`, then `make sync-platform-specs`. The upstream `-beta` specs are wrong in ways only wire probing revealed, and the corrections live in the SDK's `tools/generate/config.json`, validated by acceptance tests against a live tenant. The SDK publishes them as OpenAPI extensions that this generator reads, so no override table is duplicated here — `specs/platform/` is a mixed drop, and the mix narrowed sharply at the v0.22.0 ingest (neither v0.22.1 nor v0.22.2 moved a platform spec at all — both land entirely inside the Classic body schemas; see the repeated-element note — and **v1.0.0 moved exactly one**, `ai_governance_policies_api.json` to v2121, plus one operation into `pro_api.json`): **twelve of the seventeen specs are now v2082** — blueprints, compliance-benchmarks, declaration-reporting, both device-inventory specs, device-management-action, and all six Security Cloud specs (categories, device-groups, dns, enrollment, uem-connect, ztna). v2082 is the scope-declaration build, and it moved *every operation in every one of those twelve* by rewriting one thing (see the scope migration below), which is why the diff looks alarming and is not. -The five that did not move: `audit_api.json` and `account_partners_api.json` are **v2056** — the second inert here, its `servers` region enum having narrowed to `us` alone, which corroborates `requireUSGateway` but reaches no command, a server variable being nothing the emitter renders; `ai_governance_policies_api.json` is **v1877**; `account_licensing_api.json` is **v1671** and unchanged since; and `account_sso_api.json` is **v1671 plus one SDK wire patch** (`RAMP` on the `Region` enum — see the Jamf Account section). +The five that did not move: `audit_api.json` and `account_partners_api.json` are **v2056** — the second inert here, its `servers` region enum having narrowed to `us` alone, which corroborates `requireUSGateway` but reaches no command, a server variable being nothing the emitter renders; `ai_governance_policies_api.json` is **v2121** as of the v1.0.0 ingest (the concurrency build — see the AI Governance section); `account_licensing_api.json` is **v1671** and unchanged since; and `account_sso_api.json` is **v1671 plus one SDK wire patch** (`RAMP` on the `Region` enum — see the Jamf Account section). -Two facts from the builds those twelve came through are still worth keeping. `securitycloud_enrollment_api.json` first appeared at **v1993**, in all three environments at once, having existed in none of them at v1988. And **v1981** is where all four of the original Security Cloud specs were first published to `external/`, dropping the stage server URL, the "(STAGE)" title and the `us1` region default — inert to the generated commands (`servers[0].url`'s *path* is all `serviceSegment` reads, and it is `/securitycloud` either way), but it is why the mixed `/api`-prefixed and bare URL shapes `TestServiceSegment` pins are both live in one drop. The two coverage specs are both **v2082** now (SDK `v0.22.2`, `d4ab4f4`): `pro_api.json` is 476 paths and 700 operations (v1942 took it to 666, v2043's App Installers ingest to 690, v2051's three withdrawals to 687, and v2082's restoration of all thirteen `/v3/computers-inventory` operations to 700) and `classic_api_resource_documentation.json` is 270 paths and 589 operations, `info.version` still 11.28.0. **The capi hold is over.** v1993 withdrew 32 Classic operations and the SDK held back one of them — `POST /patchsoftwaretitles/id/{id}`, the only call that mints a `softwareTitleId`, which the Pro v3 patch-configuration endpoints require on create and which no substitute supplies: of the 1552 `name_id` values in Jamf's source catalogue, 916 are non-numeric hex-ish definition keys (`0F5`, `2DE`, `41D`) while `softwareTitleId` must be a positive numeric string per the server's own `INVALID_ID` message, and the 636 all-digit ones are rejected anyway with `400 SOFTWARE_TITLE_ID_NOT_FOUND`. `name_id` names what to subscribe to; `softwareTitleId` names a subscription. The hold ended the way it was written to end — the family reappeared in a published spec — so the hold is gone and the spec mirrors capi exactly. Worth keeping: a per-operation hold with a stated end condition is what made this survivable, where a version-wide one would have frozen the whole Classic surface for two builds. The mix is expected — the SDK re-ingests per family, and a family whose spec is byte-identical is not bumped for the sake of a version number: +Two facts from the builds those twelve came through are still worth keeping. `securitycloud_enrollment_api.json` first appeared at **v1993**, in all three environments at once, having existed in none of them at v1988. And **v1981** is where all four of the original Security Cloud specs were first published to `external/`, dropping the stage server URL, the "(STAGE)" title and the `us1` region default — inert to the generated commands (`servers[0].url`'s *path* is all `serviceSegment` reads, and it is `/securitycloud` either way), but it is why the mixed `/api`-prefixed and bare URL shapes `TestServiceSegment` pins are both live in one drop. `pro_api.json` is **v2121** and `classic_api_resource_documentation.json` **v2082** (SDK `v1.0.0`, `bed9af9`): `pro_api.json` is 477 paths and 701 operations (v1942 took it to 666, v2043's App Installers ingest to 690, v2051's three withdrawals to 687, v2082's restoration of all thirteen `/v3/computers-inventory` operations to 700, and v2121's restoration of `GET /v1/mdm/commands` to 701 — the second reversed v1942 withdrawal, a verbatim revert of the pre-v1942 entry with no schema change, scoped `device-actions:read`. **It reaches no command**: `deduplicateVersionedOps` keeps `GET /v2/mdm/commands`, which the gateway already published, so the refusal table did not move and `POST /v2/mdm/commands` stays unpublished. Worth knowing before anyone reaches for the v1 path by hand — it is a two-parameter point lookup, not a list, and the SDK's probe found the spec wrong about it in both directions: both parameters are marked optional while sending *neither* answers 400 and sending *both* answers **500**, each with an empty `errors` array and no attribution) and `classic_api_resource_documentation.json` is 270 paths and 589 operations, `info.version` still 11.28.0. **The capi hold is over.** v1993 withdrew 32 Classic operations and the SDK held back one of them — `POST /patchsoftwaretitles/id/{id}`, the only call that mints a `softwareTitleId`, which the Pro v3 patch-configuration endpoints require on create and which no substitute supplies: of the 1552 `name_id` values in Jamf's source catalogue, 916 are non-numeric hex-ish definition keys (`0F5`, `2DE`, `41D`) while `softwareTitleId` must be a positive numeric string per the server's own `INVALID_ID` message, and the 636 all-digit ones are rejected anyway with `400 SOFTWARE_TITLE_ID_NOT_FOUND`. `name_id` names what to subscribe to; `softwareTitleId` names a subscription. The hold ended the way it was written to end — the family reappeared in a published spec — so the hold is gone and the spec mirrors capi exactly. Worth keeping: a per-operation hold with a stated end condition is what made this survivable, where a version-wide one would have frozen the whole Classic surface for two builds. The mix is expected — the SDK re-ingests per family, and a family whose spec is byte-identical is not bumped for the sake of a version number: - `x-jamf-tenant-path-version` (spec root) — the URL version segment a spec's paths need but omit. The versionless form answers **403 `BAD_PERMISSIONS`**. **No spec sets it any more**: categories moved the version into its own paths in build v1353, dns and ztna followed in v1416, and build v1495 then dropped the tenant segment entirely in favour of the `X-Tenant-Id` header. The support stays because the next spec to arrive without a version prefix needs it — `normalisePlatformPaths` still consumes it — but it no longer has anything to do with tenancy. A spec that gains the prefix while *keeping* the extension would send `/v1/v1/…`, so check the paths rather than the extension when a spec is bumped. - `x-jamf-expected-status` (operation) — the success status the server really answers. Its longest-standing entry is gone: `PUT /v1/groups/{id}` had it set to 200 against a declared 204, and v1865 made the spec say 200 itself. @@ -777,7 +779,7 @@ The fix is a third key level. `platformResourceNameOverrides` is now tried `{nam #### Jamf AI Governance -Twelve generated commands from `ai_governance_policies_api.json` (SDK build v1877), wired under **`platform`, not `pro`** — `platform ai-policies` (`aip`) and `platform ai-tools` (`ait`). That is the one place this surface departs from "Platform API commands live under `pro`", and deliberately: AI Governance is not a Jamf Pro surface. It is scoped at the organization or platform-environment level, its privileges are its own (`ai-policies:{read,create,update,delete}`), and the credential this was verified with names no Jamf Pro tenant at all — `pro ai-policies` would have implied a Jamf Pro instance that need not exist. Aliases live in a new `platformAliases` map (`aliases.go`), groups under `groupPlatformAI` in `platformGroups`. +Twelve generated commands from `ai_governance_policies_api.json` (SDK build v2121), wired under **`platform`, not `pro`** — `platform ai-policies` (`aip`) and `platform ai-tools` (`ait`). That is the one place this surface departs from "Platform API commands live under `pro`", and deliberately: AI Governance is not a Jamf Pro surface. It is scoped at the organization or platform-environment level, its privileges are its own (`ai-policies:{read,create,update,delete}`), and the credential this was verified with names no Jamf Pro tenant at all — `pro ai-policies` would have implied a Jamf Pro instance that need not exist. Aliases live in a new `platformAliases` map (`aliases.go`), groups under `groupPlatformAI` in `platformGroups`. Two ingest knobs it needed, both because the paths are shaped unlike anything else in the tree: @@ -801,10 +803,14 @@ Two ingest knobs it needed, both because the paths are shaped unlike anything el - **`schema-drift` is a flag, not a boolean filter.** `schema-drift=true` narrows to drifted policies; `false` — and `maybe`, which is not rejected — returns everything. The generated `--schema-drift` only sends the parameter when true, which is correct by accident rather than design: keep it that way. - **`delete` is an archive and is not idempotent.** 204, then the policy is invisible to `get` and `list` alike, and a second `delete` answers `404 POLICY_NOT_FOUND`. - **`href` on `create` and `publish` is populated by the service, not by the gateway's href-injection plugin**, so the gzip bug that nulls `href` on Security Cloud creates does not apply here. Neither call returns a `Location` header. -- **The error envelope is the service's own: `{traceId, errors}` with no `httpStatus`**, unlike the gateway's `{httpStatus, traceId, errors}`. Codes are specific and worth knowing: `TOOL_ID_UNKNOWN`, `SCHEMA_VERSION_UNKNOWN`, `SCHEMA_VALIDATION_FAILED`, `POLICY_NOT_FOUND`, `NO_DRAFT_TO_PUBLISH`. -- **One undeclared status.** `GET /tools/{toolId}/schemas/{schemaVersion}` for an unknown version answers **422 `SCHEMA_VERSION_UNKNOWN`**, where the spec declares only 404 for that operation. `GET /tools/{toolId}` for an unknown tool does answer 404. A non-UUID `policyId` is a 404, not a 400. +- **The error envelope is the service's own and v2121 finally declared all of it.** It was recorded here as `{traceId, errors}` with no `httpStatus`, unlike the gateway's `{httpStatus, traceId, errors}`; v2121 declares `httpStatus` **required** on `ApiError` and the SDK's probes confirm the wire has always sent it — so this was a spec gap rather than a second envelope shape, and the two are the same shape after all. Codes are specific and worth knowing: `TOOL_ID_UNKNOWN`, `SCHEMA_VERSION_UNKNOWN`, `SCHEMA_VALIDATION_FAILED`, `POLICY_NOT_FOUND`, `NO_DRAFT_TO_PUBLISH`. +- **~~One undeclared status.~~ Declared at v2121.** `GET /tools/{toolId}/schemas/{schemaVersion}` for an unknown version answers **422 `SCHEMA_VERSION_UNKNOWN`**, which the spec omitted and now declares alongside its 404. Nothing in the CLI reads a response declaration, so this is diagnostic only — the same class as ZTNA's 409 family. `GET /tools/{toolId}` for an unknown tool does answer 404. A non-UUID `policyId` is a 404, not a 400. - **`ai-tools schema` returns ~250 KB of JSON Schema** for `com.anthropic.claudecode`. There is no table shape for it and none is offered; it is a raw JSON read. +- **v2121 added an ETag/If-Match concurrency protocol, and this CLI can reach only the read half.** `PolicyDetail` gains a nullable `version` counter, the detail `GET` returns it as a strong `ETag`, and `PATCH` takes an `If-Match` precondition answering **409 `POLICY_VERSION_CONFLICT`** when the policy has moved on. Every part is enforced on the wire (SDK probe 2026-09-09): the server compares the *number* and tolerates bare `3`, strong `"3"` and weak `W/"3"` alike, `*` and an absent header are unconditional, `garbage` is a 400 — so the header is parsed, not ignored — and only a comma-separated list is refused. **Every `PATCH` increments `version` whether or not it changes anything**, and `version` is independent of `currentVersionNumber`, which only `publish` moves. Two consequences here. `version: null` is not an unpopulated field but a **legacy document with no ETag at all** — both policies predating the rollout answer null and send no `ETag` — so those can never be updated conditionally by anyone. And the platform emitter renders **query parameters only**, so a declared `in: header` parameter produces no flag and no error (the same silence that correctly costs audit's `X-Environment-Id` a flag): `platform ai-policies patch` and `apply` send no `If-Match` and are therefore always unconditional, and a concurrent draft edit still silently wins last-writer. That is the one real capability gap this ingest opens; closing it means teaching the emitter header parameters, which the SDK had to do for the same reason — its generator had no `In` concept at all, so an `If-Match` entry would have been emitted as a **query** key the server ignores, turning a conditional update into an unconditional one with nothing failing. +- **v2121 also declared a `409 POLICY_IN_USE` on `delete`, and it is unverified — the last probe says the opposite.** The spec now says archiving is refused while a blueprint references the policy, and the generated `--help` for `platform ai-policies delete` says so verbatim. But the 2026-08-30 full-surface probe found archiving a blueprint-referenced policy answering a clean **204** and leaving the blueprint pointing at a policy nothing can read — no referential guard, the opposite of Security Cloud's `*_REFERENCED_BY_*` 409s — and nothing has re-probed it since. So the help text is now ahead of the wire in the *safe* direction (it warns about a refusal that may not happen) rather than the dangerous one, but treat the guard as a declaration and not a fact. +- **`x-preview: true` and `x-preview-owners` arrived at the spec root, and `Jamf-Preview: true` on every 2xx.** Nothing reads either: no generator consumes a root extension it does not know, and the CLI never inspects response headers. Worth knowing the whole policies API is formally a preview surface, which is the reason to expect its shape to keep moving. - **`ai/governance/visibility` is routed** (403 `BAD_PERMISSIONS` on this credential, which holds the policies grants) and still has no published spec — the tell that `ai/governance` is the product and `policies`/`visibility` are capabilities beneath it. The hyphenated `ai-governance/policies` remains a bare `404 page not found`, which is what v1877 fixed in the spec's `servers` block. +- **`400 BadRequest` is now declared on the four reads that lacked it** (`list`, `versions`, `version`, `ai-tools schema`). Diagnostic only, as above. #### Jamf Account and Audit diff --git a/go.mod b/go.mod index 5f1368c9..e91afc76 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Jamf-Concepts/jamf-cli go 1.27.0 require ( - github.com/Jamf-Concepts/jamfplatform-go-sdk v0.20.1 + github.com/Jamf-Concepts/jamfplatform-go-sdk v1.0.0 github.com/Jamf-Concepts/jamfprotect-go-sdk v0.8.0 github.com/Jamf-Concepts/jamfschool-go-sdk v0.1.2 github.com/getkin/kin-openapi v0.149.0 @@ -39,7 +39,7 @@ require ( github.com/segmentio/encoding v0.5.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/net v0.58.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/oauth2 v0.37.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.15.0 // indirect diff --git a/go.sum b/go.sum index 98da8ea6..be017890 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Jamf-Concepts/jamfplatform-go-sdk v0.20.1 h1:nu4pRGyBMW/YQkFQVDmzH4Pr5kbFZVZm/Xz27YGEVBg= -github.com/Jamf-Concepts/jamfplatform-go-sdk v0.20.1/go.mod h1:gUL5YSSzFcEQ758X7/b8sAxVLEoHq4DcdrmRYYyxbiU= +github.com/Jamf-Concepts/jamfplatform-go-sdk v1.0.0 h1:cyvDo+ZLtxykRZU/e4sT9vqBbjrLUoPVW+1m4sne1bA= +github.com/Jamf-Concepts/jamfplatform-go-sdk v1.0.0/go.mod h1:xumNGBezXfsqGGLQTaFRkdJH1FxDFBGLlsbZFYAJq3o= github.com/Jamf-Concepts/jamfprotect-go-sdk v0.8.0 h1:AiP8KiPpTMLnAEY4E9T8PtBOihVMjeQ3/lnLcpbR/yc= github.com/Jamf-Concepts/jamfprotect-go-sdk v0.8.0/go.mod h1:IoRB8uP1p0XxxFr/ZbP4+HSmoYd66LES1l0xev0GaxM= github.com/Jamf-Concepts/jamfschool-go-sdk v0.1.2 h1:+YHY9leMdyP6oEDJtqIrLNTLnVVuyRW+kN5tZBvwmQY= @@ -91,8 +91,8 @@ golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= +golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= diff --git a/internal/commands/platform/generated/ai-policies.go b/internal/commands/platform/generated/ai-policies.go index 63bf6ff8..f07779ff 100644 --- a/internal/commands/platform/generated/ai-policies.go +++ b/internal/commands/platform/generated/ai-policies.go @@ -192,7 +192,7 @@ func newAiPoliciesDeleteCmd(cliCtx *registry.CLIContext) *cobra.Command { cmd := &cobra.Command{ Use: "delete ", Short: "Archive an AI governance policy (soft delete)", - Long: "Archives the policy. All published versions are retained for audit trail integrity.", + Long: "Archives the policy. All published versions are retained for audit trail integrity. Returns 409 (POLICY_IN_USE) if one or more Blueprints currently reference this policy — remove the policy from all Blueprints before archiving.", Annotations: map[string]string{"jamf:destructive": "true", "jamf:privileges": "ai-policies:delete", "jamf:api": "platform-gateway", "jamf:scopes": "environment"}, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/internal/commands/platform/generated/provenance.go b/internal/commands/platform/generated/provenance.go index 93ebee31..45849f03 100644 --- a/internal/commands/platform/generated/provenance.go +++ b/internal/commands/platform/generated/provenance.go @@ -15,7 +15,7 @@ var Sources = []SpecSource{ {File: "specs/platform/account_licensing_api.json", SHA256: "12dc1dd124d8a38706ac6be70882c0d915be67abef424bf01558510ed9a7e97c"}, {File: "specs/platform/account_partners_api.json", SHA256: "2cd9861989bddb7774a3e3b7c02413c09509758954d697b92936e0286ed774b6"}, {File: "specs/platform/account_sso_api.json", SHA256: "1666b749cd3af48e15d4b7ab0f6360665205767541eb63fd328976baf6214b09"}, - {File: "specs/platform/ai_governance_policies_api.json", SHA256: "7c0ba60fecae405a33f290073a7868c61dd5c08b2bca3d2c987f3dc074f91a0d"}, + {File: "specs/platform/ai_governance_policies_api.json", SHA256: "c8aa7179373f0798b4f03c16c397ddc8a6ee3b0f4baec33509dfb0435de1ec53"}, {File: "specs/platform/audit_api.json", SHA256: "91c2bc728040703da48194160da8605de3b9c35c734fe04f93a8a7cf88359881"}, {File: "specs/platform/blueprints_api.json", SHA256: "43276c66813acb8a360ce9ae41a2b6062bc33e3ab1d327f11beeba5dac943654"}, {File: "specs/platform/compliance_benchmark_engine.json", SHA256: "5db95f8e71eee86fcf6677f1ec246a800fffbbe02715e72fdff80f4b0859f92d"}, diff --git a/internal/gateway/coverage_gen.go b/internal/gateway/coverage_gen.go index 31d2722b..af0e7c85 100644 --- a/internal/gateway/coverage_gen.go +++ b/internal/gateway/coverage_gen.go @@ -21,7 +21,7 @@ package gateway const ( ProAPIVersion = "11.31.0" ClassicAPIVersion = "11.28.0" - SDKCommit = "d4ab4f4" + SDKCommit = "bed9af9" ) // unserved is sorted by path then method. Paths are gateway-form with every @@ -389,6 +389,7 @@ var scopeRules = []scopeRule{ {Method: "GET", Path: "/pro/v1/mdm-renewal/device-common-details/{}", Scopes: []string{"device-actions:read"}}, {Method: "DELETE", Path: "/pro/v1/mdm-renewal/renewal-strategies/{}", Scopes: []string{"device-actions:execute"}}, {Method: "GET", Path: "/pro/v1/mdm-renewal/renewal-strategies/{}", Scopes: []string{"device-actions:read"}}, + {Method: "GET", Path: "/pro/v1/mdm/commands", Scopes: []string{"device-actions:read"}}, {Method: "POST", Path: "/pro/v1/mdm/renew-profile", Scopes: []string{"device-actions:execute"}}, {Method: "GET", Path: "/pro/v1/mobile-device-enrollment-profile/{}/download-profile", Scopes: []string{"enrollment-profiles:read"}}, {Method: "GET", Path: "/pro/v1/mobile-device-extension-attributes", Scopes: []string{"extension-attributes:read"}}, diff --git a/specs/classic/schemas.json b/specs/classic/schemas.json index 62a7ce6f..d9c15b1b 100644 --- a/specs/classic/schemas.json +++ b/specs/classic/schemas.json @@ -8,7 +8,7 @@ "spec": "classic_api_resource_documentation.json", "title": "Classic API", "version": "11.28.0", - "sdkCommit": "d4ab4f4", + "sdkCommit": "bed9af9", "schemas": 165, "resources": 44, "unresolved": 1 diff --git a/specs/gateway/coverage.json b/specs/gateway/coverage.json index 1d4f18ad..0c217e53 100644 --- a/specs/gateway/coverage.json +++ b/specs/gateway/coverage.json @@ -5,8 +5,8 @@ "file": "pro_api.json", "title": "Jamf Pro API", "version": "11.31.0", - "paths": 476, - "operations": 700 + "paths": 477, + "operations": 701 }, "classic": { "file": "classic_api_resource_documentation.json", @@ -15,7 +15,7 @@ "paths": 270, "operations": 589 }, - "sdkCommit": "d4ab4f4" + "sdkCommit": "bed9af9" }, "spec": { "/pro/devices/extensionAttributes": [ @@ -747,6 +747,9 @@ "DELETE", "GET" ], + "/pro/v1/mdm/commands": [ + "GET" + ], "/pro/v1/mdm/renew-profile": [ "POST" ], @@ -4066,6 +4069,11 @@ "device-actions:read" ] }, + "/pro/v1/mdm/commands": { + "GET": [ + "device-actions:read" + ] + }, "/pro/v1/mdm/renew-profile": { "POST": [ "device-actions:execute" diff --git a/specs/platform/ai_governance_policies_api.json b/specs/platform/ai_governance_policies_api.json index e1d31b32..b353245b 100644 --- a/specs/platform/ai_governance_policies_api.json +++ b/specs/platform/ai_governance_policies_api.json @@ -1,6 +1,26 @@ { "components": { + "headers": { + "JamfPreview": { + "description": "Present on every successful (2xx) response from a preview endpoint. See\nthe API description for preview status and expected graduation.\n", + "schema": { + "enum": [ + true + ], + "type": "boolean" + } + } + }, "parameters": { + "IfMatch": { + "description": "Optimistic-lock precondition. Supply the ETag from a prior GET (e.g. `\"7\"`) to\nmake the update conditional on the policy still being at that version; a mismatch\nyields 409 (POLICY_VERSION_CONFLICT). A weak validator (`W/\"7\"`) is also accepted.\nOmit the header or send `*` for an unconditional update.\n", + "example": "\"7\"", + "in": "header", + "name": "If-Match", + "schema": { + "type": "string" + } + }, "PageParam": { "description": "0-based page number", "in": "query", @@ -121,6 +141,14 @@ }, "type": "array" }, + "httpStatus": { + "description": "HTTP status of the response", + "example": 404, + "format": "int32", + "maximum": 599, + "minimum": 400, + "type": "integer" + }, "traceId": { "description": "Trace ID for request correlation", "example": "abc123def456", @@ -128,6 +156,7 @@ } }, "required": [ + "httpStatus", "traceId", "errors" ], @@ -319,6 +348,13 @@ }, "settings": { "$ref": "#/components/schemas/VendorSettings" + }, + "version": { + "description": "Optimistic-lock counter, incremented on each PATCH. Surfaced as the strong\nETag response header and used as the If-Match precondition. Null for legacy\ndocuments that predate versioning.\n", + "example": 7, + "format": "int64", + "nullable": true, + "type": "integer" } }, "required": [ @@ -708,7 +744,15 @@ } } }, - "description": "Paginated list of policies" + "description": "Paginated list of policies", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -761,6 +805,9 @@ }, "description": "Policy created", "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + }, "Location": { "description": "URL of the created policy", "schema": { @@ -800,7 +847,7 @@ }, "/v1/policies/{policyId}": { "delete": { - "description": "Archives the policy. All published versions are retained for audit trail integrity.\n\n**Required Permissions:** `ai-policies:delete`", + "description": "Archives the policy. All published versions are retained for audit trail\nintegrity. Returns 409 (POLICY_IN_USE) if one or more Blueprints currently\nreference this policy — remove the policy from all Blueprints before archiving.\n\n\n**Required Permissions:** `ai-policies:delete`", "operationId": "archivePolicy", "parameters": [ { @@ -809,7 +856,12 @@ ], "responses": { "204": { - "description": "Policy archived" + "description": "Policy archived", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -819,6 +871,26 @@ }, "404": { "$ref": "#/components/responses/NotFound" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "Policy is referenced by one or more Blueprints (POLICY_IN_USE)" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "Upstream Blueprints service unavailable during the reference check" } }, "security": [ @@ -837,7 +909,7 @@ ] }, "get": { - "description": "Returns the policy with its current draft settings. schemaDrift indicates whether\nthe draft was authored against an older schema version. hasDraft indicates whether\nunpublished changes exist.\n\n\n**Required Permissions:** `ai-policies:read`", + "description": "Returns the policy with its current draft settings. schemaDrift indicates whether\nthe draft was authored against an older schema version. hasDraft indicates whether\nunpublished changes exist.\n\nThe response carries a strong ETag derived from the optimistic-lock version\ncounter (omitted for legacy documents that predate versioning). Pass this value\nback in an If-Match header on PATCH to guard against lost updates.\n\n\n**Required Permissions:** `ai-policies:read`", "operationId": "getPolicy", "parameters": [ { @@ -853,7 +925,19 @@ } } }, - "description": "Policy detail" + "description": "Policy detail", + "headers": { + "ETag": { + "description": "Strong validator for the policy's current version, e.g. `\"7\"`. Absent for\nlegacy documents that have no version counter. Echo it in the If-Match\nheader on a subsequent PATCH.\n", + "example": "\"7\"", + "schema": { + "type": "string" + } + }, + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -886,11 +970,14 @@ } ], "patch": { - "description": "Validates settings against the vendor schema and updates the draft settings on\nthe policy. Does NOT create a published version. Call POST /policies/{policyId}/publish\nto snapshot the draft into an immutable version.\n\n\n**Required Permissions:** `ai-policies:update`", + "description": "Validates settings against the vendor schema and updates the draft settings on\nthe policy. Does NOT create a published version. Call POST /policies/{policyId}/publish\nto snapshot the draft into an immutable version.\n\nSupports optimistic concurrency control for concurrent draft edits. Supply the\nETag returned by GET in an If-Match header; if the policy's version has since\nadvanced, the write is rejected with 409 (POLICY_VERSION_CONFLICT) and no changes\nare made. Omit If-Match (or send `*`) for an unconditional update.\n\n\n**Required Permissions:** `ai-policies:update`", "operationId": "updatePolicy", "parameters": [ { "$ref": "#/components/parameters/X-Environment-Id" + }, + { + "$ref": "#/components/parameters/IfMatch" } ], "requestBody": { @@ -905,7 +992,12 @@ }, "responses": { "204": { - "description": "Draft settings saved" + "description": "Draft settings saved", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } }, "400": { "$ref": "#/components/responses/BadRequest" @@ -919,6 +1011,16 @@ "404": { "$ref": "#/components/responses/NotFound" }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "The policy was modified by another writer since the supplied If-Match version\n(POLICY_VERSION_CONFLICT). Re-read the policy to obtain the current ETag and\nretry.\n" + }, "422": { "$ref": "#/components/responses/UnprocessableEntity" } @@ -957,7 +1059,12 @@ } } }, - "description": "Deployment state for all blueprints referencing this policy" + "description": "Deployment state for all blueprints referencing this policy", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -1025,6 +1132,9 @@ }, "description": "Version published", "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + }, "Location": { "description": "URL of the created policy version", "schema": { @@ -1097,7 +1207,15 @@ } } }, - "description": "Paginated list of policy versions" + "description": "Paginated list of policy versions", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -1148,7 +1266,15 @@ } } }, - "description": "Policy version detail" + "description": "Policy version detail", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -1209,7 +1335,12 @@ } } }, - "description": "List of available tools" + "description": "List of available tools", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -1252,7 +1383,12 @@ } } }, - "description": "Tool detail" + "description": "Tool detail", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -1316,7 +1452,15 @@ } } }, - "description": "Vendor JSON Schema" + "description": "Vendor JSON Schema", + "headers": { + "Jamf-Preview": { + "$ref": "#/components/headers/JamfPreview" + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -1333,6 +1477,16 @@ } }, "description": "Tool or schema version not found" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "Schema version is not a known version for this tool (SCHEMA_VERSION_UNKNOWN)" } }, "security": [ @@ -1404,6 +1558,10 @@ "name": "tools" } ], + "x-preview": true, + "x-preview-owners": [ + "ai-policy-builder-backend" + ], "x-scope-types": [ "environment" ]