GitHub
11/08/2026, 7:10 PMosctrl-cli and pkg/apiclient still sent the old on/off flags. So operators couldn't manage any of this from scripts or the CLI, in either API mode or direct-database mode.
What changes
Tiers and tag grants
• user change-permissions and user reset-permissions gain:
• --query-tier and --carve-tier (none|tags|standard|full);
• `--scope-tags`: tag names, resolved per environment. "" clears the grants.
• The matching on/off flag is always set with its tier.
• Unknown tier names, unknown tags, tags from another environment and sensitive tags (sensitive tag cannot be a scope tag) are refused before anything is written, in both modes. --admin combined with a none tier is also refused.
• change-permissions keeps its additive on/off behavior. --query-tier sets the tier outright. Without it, a tag-scoped user stays tag-scoped.
• reset-permissions is a full replace in both modes: an unset tier becomes standard when the flag is on and none when it is off, and tag grants are cleared unless --scope-tags is given.
• show-permissions and all-permissions add Query tier, Carve tier and Scope tags columns.
• apiclient.SetUserPermissions sends query_tier, carve_tier and `scope_tags`: nil omits the field, and an empty list clears the grants.
Sensitive tags
• tag add and tag edit take --sensitive. On edit, leaving the flag out keeps the stored value.
• Database mode removes the tag from every scope grant when it is marked sensitive, as the server does, and records it in the audit entry.
• tag list and tag show show a Sensitive column.
Sensitive carve paths
• New environment sensitive-paths list|add|remove (--path, --id), in both modes.
• New apiclient.ListSensitivePaths, AddSensitivePath and DeleteSensitivePath.
• Database mode applies the same normalization and validation as the server (for example, path must be absolute). It refuses another environment's ID, and writes the same audit entries.
Fixed along the way
• tag edit corrupted fields that weren't passed, in both modes. Without -t it rewrote the type to Unknown and reset the custom value, and without -c it picked a random color. It also rewrote non-canonical custom values that nobody set. Edit now keeps every stored field not given on the command line. In API mode it reads the tag first and sends the stored values back.
Security and operational impact
• No server changes. API mode only calls existing endpoints, and the server still enforces everything.
• Behavior changes:
• reset-permissions in API mode now also clears tag grants and resets tiers from the on/off flags. It previously kept both. Database mode already wiped them; both modes now agree.
• tag edit no longer changes type, custom value or color unless they are passed.
• Database mode still differs from the server in one way: it does not cancel pending queries or carves when a tag or path becomes sensitive. The CLI's direct-database mode is trusted operator tooling, and results are still hidden when read.
• --scope-tags in API mode needs env admin on that environment to look up tag names. The flag's help text says so.
• The interactive shell is out of scope.
Validation
• Parity tests run each command in both modes and compare the stored results:
• tiers and scope tags, including the refusal cases with nothing written;
• reset as a full replace, starting from a tag-scoped user with grants;
• change-permissions -q keeping a tag-scoped tier;
• --admin refused with a none tier;
• `tag add/edit --sensitive`;
• edit keeping type, custom, color, icon and description, including non-canonical custom values;
• scope grants removed when a tag is marked sensitive;
• sensitive paths: normalization, a relative path refused, and another environment's ID refused.
• API client tests check scope_tags omitted/`[]`/values, the tag sensitive field, and the sensitive-path URLs and methods.
• go vet ./cmd/cli ./pkg/apiclient is clean and go test ./... passes on an uncached run.
jmpsec/osctrlGitHub
11/08/2026, 7:59 PM