Skip to content

feat(governance): let a policy name the feature content hosts - #23717

Open
iamwhatever wants to merge 1 commit into
mainfrom
feat/ee-list-feature-video-hosts
Open

iamwhatever wants to merge 1 commit into
mainfrom
feat/ee-list-feature-video-hosts

Conversation

@iamwhatever

@iamwhatever iamwhatever commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: A security policy can name the exact hosts feature-video content may be downloaded from, and no request goes to any other host.

capabilities.feature_videos_download is an on/off gate with no list (RFC crew-enterprise-edition, appendix rows "Feature content download" partial and "Feature content hosts" feature_videos_download.hosts, no). A fleet that mirrors the content cannot say "fetch from our mirror only".

Why it matters

A managed fleet that permits feature content from its own mirror must be able to refuse the vendor CDN and every other host, including a host a mirror redirects to.

Not a goal

  • No enterprise-only branch and no change to _query_level: with no policy, or a policy without the list, behaviour is unchanged.
  • No change to clips already on disk (they keep playing), to the CSP, or to the URL override.
  • The other three partial controls are separate PRs.

What changed (motivation → approach → change)

Goal → the existing CapabilityGate inner-ruleset shape → one catalog row change, one probe, three request sites.

  • governance_parts/catalog.py: capabilities.feature_videos_download gains scope_matchers={"hosts": "host"}. Queried as hosts:<host>.
  • governance_parts/matching.url_policy_hosts: every host a client could connect to for a url (the standard library plain and host-percent-decoded, urllib3, yarl; non-ASCII in IDNA form); host_denied permits only when every reading is permitted. The same helper is added identically in feat(governance): let a policy name the OTLP collector hosts #23716 and feat(governance): let a policy name decision models, points and endpoints #23719.
  • feature_videos_manifest.host_denied(url): audited, fail-closed, on the pinned dashboard:ui surface like download_denied. It does not touch the download_denied memo, so one refused host does not report the feature off.
  • asset_downloader.build_opener gains an optional redirect_allowed predicate, asked about every redirect hop. The manifest fetch passes host_denied, because an operator mirror may redirect across hosts. Callers that pass nothing are unchanged.
  • Docs: governance.md (feature-video section) and the user doc feature-videos.md.

Call sites wired:

  • feature_videos_manifest.fetch_manifest: before each manifest request in the walk.
  • feature_videos_manifest._fetch_json: every redirect hop of that request.
  • feature_videos_cache.FeatureVideoCache._download_entry_into: before the poster request and before the clip request (HOST_DENIED fails that entry only; those transfers already refuse cross-host redirects).

Backwards compatibility

Compatible: with no policy, or a policy that does not name hosts, every host is permitted, and build_opener callers that pass no redirect_allowed get the same handler class as before.

Removes nothing: the list only narrows when a policy names it.

Tests

test/test_governance_list_feature_video_hosts.py: the key parses with the host matcher; a list allowing X permits X and denies Y; gate off denies; no policy or no list permits. Call sites: host_denied follows the list and audits hosts:<host>; it permits with no policy; the manifest walk makes no request to an unlisted host; a mirror redirect to an unlisted host is refused hop by hop; a clip on an unlisted host is not downloaded.

Proof: not provable -- other: new feature; the tests assert behaviour that main does not have

Manual verification

N/A — unit coverage drives the real evaluator and each request site with the network stubbed.

Related Issues

Refs #23206. RFC: #23208.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

Contributions are accepted under the project's Apache-2.0 license.

@iamwhatever
iamwhatever requested review from a team and KiroCrewCaptain as code owners October 24, 2026 09:31
@iamwhatever
iamwhatever requested a review from CrysisDeu October 24, 2026 09:31
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5) — ⏭️ skipped

Revision 05bd4d732bb0efedaf98029d65e603abb35b61c0 touches no user-facing surface (no changes under website/ or committed screenshots), so the UX review was skipped. Does not block merge.

@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5) — ✅ PASS

Design-level review of 05bd4d732bb0efedaf98029d65e603abb35b61c0 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

Design-Verdict: PASS

Nothing blocks: the change only narrows when a policy names hosts, and it reuses the existing CapabilityGate inner-ruleset shape.

Notes

[DESIGN-REVIEWED] 05bd4d7

Description read

[DESCRIPTION-READ] 6af6593106574dd1d0d15f80c681150158b22e953d5ce20f8831c18129bd2ebd

That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5) — ✅ PASS

Premise-level review of 05bd4d732bb0efedaf98029d65e603abb35b61c0 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

First-Principles-Verdict: PASS

Nothing blocks. A non-draft RFC row covers the hosts list. Two riders the description doesn't mention add new surface, so they are notes.

Notes

  • Item 5 — undeclared, one consumer, generalized: archetypes._refuse_unmatchable_host_entries now makes a policy fail to load when any capability's host list has an entry in the wrong form. The description doesn't mention it. Grepping scope_matchers=.*"host" in catalog.py finds exactly one such row: this one. Subtraction: limit the check to capabilities.feature_videos_download.hosts, or drop it and keep the ""-matches-nothing behaviour the docstring already describes.
  • Item 2 — oversized: url_policy_hosts is a second host extractor for the host matcher. matching._url_host already does that job for network.egress (4 call sites in tool_calls.py), and the diff says egress "keeps its own parsing". The description says the helper is "added identically in feat(governance): let a policy name the OTLP collector hosts #23716 and feat(governance): let a policy name decision models, points and endpoints #23719". Subtraction: land the helper once, in whichever PR merges first, and rebase the other two onto it instead of carrying three copies.

What this change ships

Inventory (6 items) — 4 justified

Intent: let a managed fleet limit feature-video downloads to named hosts, such as its own mirror (ADDITION).

  1. A policy's feature_videos_download can name allowed or denied hosts as hosts — justified
  2. A URL's host is checked in every form a client could read it, and denied unless every form is permitted — oversized
  3. The manifest walk stops before requesting an unlisted host — justified
  4. A manifest redirect to an unlisted host is refused on every hop — justified
  5. A policy fails to load when a capability host entry can never match — undeclared, one consumer, generalized
  6. A poster or clip on an unlisted host fails that entry only — justified

[FIRST-PRINCIPLES-REVIEWED] 05bd4d7

Description read

[DESCRIPTION-READ] ac23effc58b904cbfad1460c90ab83fa64aa3f9afa87900753794d171f78200e

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review — 🔴 changes requested (blocking)

Reviewed 05bd4d732bb0efedaf98029d65e603abb35b61c0 — this comment is updated in place on each push.

_unmatchable_glob rejects host globs that do match real hosts, so a capability hosts list containing * (or 1*, ?, 0x*, 2001:0:0:1::*) makes the whole security policy fail to load.

BLOCKING — src/kiro_crew/platform/governance_parts/archetypes.py:427
if all(re.fullmatch(r"[0-9*?]+", label) for label in labels): / if len(labels) == 1: / return "is an integer IP glob; write the dotted IPv4 form"
Trigger: an administrator's security_policy.json contains "feature_videos_download": {"scopes": {"hosts": {"mode": "allow", "allow": ["*"]}}}, or a deny list with "*". The path is parse_policy → CapabilityGate.from_dict → _refuse_unmatchable_host_entries → _unmatchable_glob("*"). The label "*" fully matches [0-9*?]+, and since it is the only label, the integer-IP reason is returned. The 0x* check at line 425 and the IPv6 0:0 run check at line 417 make the same mistake: 2001:0:0:1::* is refused even though the canonical address 2001:0:0:1::1 matches it.
Outcome: parse_policy raises PlatformCompositionError capability scope 'hosts' allow[0] is an integer IP glob (reproduced). _match_host uses fnmatch, where * also matches dots, so the entry matches every host. A valid enterprise ceiling therefore fails to load, and with boot.fail_closed startup aborts.
Anchor: defect
Fix: run the integer and hex IP-shape checks only on globs with no *, and require at least one literal digit. Refuse a 0:0 run only when it cannot be the uncompressed part of a canonical address: the glob has no ::, or the run is longer than the :: it already uses.

[BLOCK-MERGE] 05bd4d7
[OPUS-REVIEWED] 05bd4d7

Verdict parsed from the review's SHA-scoped output markers for commit 05bd4d732bb0efedaf98029d65e603abb35b61c0.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 05bd4d732bb0efedaf98029d65e603abb35b61c0 false-positive: <reason, 20 to 500 characters>

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review — 🔴 changes requested (blocking)

GPT 6.1 found at least one blocking issue that must be resolved before merging 05bd4d732bb0efedaf98029d65e603abb35b61c0. 1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands.

This comment is updated in place on each push.

BLOCKING -- src/kiro_crew/platform/governance_parts/archetypes.py:424 -- Valid host globs abort gateway startup

if all(re.fullmatch(r"[0-9*?]+", label) for label in labels):
Trigger: A home policy, with no higher-tier policy, sets hosts: {"mode": "deny", "deny": ["*"]}. Gateway startup reaches load_security_policy → parse_policy → CapabilityGate.from_dict → _refuse_unmatchable_host_entries → _unmatchable_glob, which rejects * as an integer-IP glob.
Outcome: PlatformCompositionError aborts startup instead of applying the deny-all host rule. The same validation also rejects 2001:db8:0:0:*, although it matches the canonical host 2001:db8:0:0:1::.
Anchor: residual/crash-data-loss-corruption
Fix: Accept wildcard-only patterns and avoid applying literal IPv6 compression restrictions to globs; reject only patterns that cannot match a canonical host.

[BLOCK-MERGE] 05bd4d7
[GPT-REVIEWED] 05bd4d7

Adjudication (Opus 5.5) — is blocking on each finding proportionate?

F1: the condition is ordinary, not extreme. A deny-all host list (deny: ["*"]) is a natural thing for an operator to write. src/kiro_crew/platform/governance_parts/archetypes.py:455-456 sends any entry containing * to _unmatchable_glob. There, "*" gives labels == ["*"], which matches [0-9*?]+ (archetypes.py:424) and has length 1, so it is refused as an integer IP glob (archetypes.py:425-426).

The IPv6 case is also real. The check at archetypes.py:417-418 rejects 2001:db8:0:0:* for its 0:0 run, yet 2001:db8:0:0:1:: is a canonical address that glob matches.

The refusal raises PlatformCompositionError (archetypes.py:471), which fails closed and visibly; the only way out is for the operator to rewrite the policy. A FLAG needs conditions too extreme to occur in practice, and these are common, so the record for one cannot be completed.

[ADJUDICATION] 05bd4d732bb0efedaf98029d65e603abb35b61c0 total=0 uphold=0 downgrade=0
[GPT-ADJUDICATED] 05bd4d732bb0efedaf98029d65e603abb35b61c0

[ADJUDICATION-FENCED] 05bd4d732bb0efedaf98029d65e603abb35b61c0 fenced=1 flagged=0
UPHOLD-FENCED F1 src/kiro_crew/platform/governance_parts/archetypes.py:424 -- A bare `*` host entry, which is a common deny-all policy, is refused as an integer IP glob, so gateway startup aborts on an ordinary operator config.
[GPT-ADJUDICATED-FENCED] 05bd4d732bb0efedaf98029d65e603abb35b61c0

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 05bd4d732bb0efedaf98029d65e603abb35b61c0 false-positive: <reason, 20 to 500 characters>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 24, 2026
@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from 19ccf92 to 89e6c22 Compare October 24, 2026 09:58
@iamwhatever

Copy link
Copy Markdown
Contributor Author

Fixed in 89e6c22: GPT BLOCKING on feature_videos_manifest.py (a debug traceback could quote a credentialed mirror URL). The probe now logs only the exception type, with no exc_info; the denial and the SEL row are unchanged. Test: test_a_probe_error_logs_no_url. In the same push, the host-reading class raised on #23716 and #23719 is closed here too: host_denied asks every reading of the host (url_policy_hosts) and permits only when all are permitted (TestUrlReadings).

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 24, 2026
@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from 89e6c22 to 656b9e1 Compare October 24, 2026 10:41
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Oct 24, 2026
@iamwhatever

Copy link
Copy Markdown
Contributor Author

Fixed in 656b9e1: GPT BLOCKING (matching.py:295, a percent-encoded redirect target %65vil.example that urllib decodes). This is the same class as #23716 and #23719, so the approach changed there too: url_policy_hosts asks each client's own parser, including the standard library's percent-decoded reading, urllib3 and yarl, IDNA-encodes each, and refuses any non-plain host under an allow list. Test: test_a_percent_encoded_redirect_target_is_refused.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 24, 2026
@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from 656b9e1 to db41ad4 Compare October 24, 2026 11:37
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Oct 24, 2026
@iamwhatever

Copy link
Copy Markdown
Contributor Author

db41ad4 carries the shared url_policy_hosts round-3 fixes from #23716 and #23719, with no change to this PR's call sites: the host part alone is percent-decoded, and a valid non-ASCII name is asked in its IDNA form.

@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from db41ad4 to 6a6d928 Compare October 24, 2026 13:09
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 24, 2026

@cruisercohen cruisercohen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed d83c076895 merged onto current main (56b568a2be). Comment only: this is governance scope, so it gets no approval from us.

Default unchanged. This adds an opt-in hosts list (host matcher) to the existing capabilities.feature_videos_download row. The row keeps capability_default=True, the evaluator and CONTRACT_VERSION are untouched, and with no policy, or a policy without a hosts ruleset, every host is permitted as before (test_host_denied_permits_without_a_policy, test_no_policy_or_no_list_permits_as_before). The list can only narrow:

  • host_denied runs before each manifest request in the walk and before each poster and clip request. It permits only when every reading of the host is permitted, and it fails closed on a probe error, logging the exception type only (no URL).
  • build_opener(redirect_allowed=...) holds every manifest redirect hop to the same list, on top of the existing https and same-host pins. The handler is constructed only when a guard is passed, so other callers get the same handler class as before.
  • A refused host fails that one entry and leaves the download_denied memo alone, so it can't switch the whole feature off.

Checks run. On the merge worktree, the new suite plus the feature-video and asset-downloader suites: 542 pass. Three test_feature_videos_boot_path failures are a local missing markdown_it module and fail the same way without this PR. Mutations:

  • host_denied always permits: 9 red.
  • Probe error permits instead of denying: 1 red.
  • Redirect guard dropped from the manifest opener: test_a_mirror_redirect_to_an_unlisted_host_is_refused red.

Non-blocking note. The url_policy_hosts / _canonical_ip / _PLAIN_HOST_RE hunk in governance_parts/matching.py is byte-identical to #23719's. _match_host itself is unchanged from main.

buluoray
buluoray previously approved these changes Oct 24, 2026

@buluoray buluoray left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at d83c076895a4c8cc2eabe95a9f89f197a05975d6.

What this gives a fleet: a policy can now pin which hosts the feature-video catalog and clips may come from, including a mirror named by KIROCREW_FEATURE_VIDEOS_MANIFEST_URL. A request to any other host is never made.

Checked against source:

  • capabilities.feature_videos_download only gains a hosts ruleset (host matcher) as a SCOPE_CATALOG data row. The evaluator and CONTRACT_VERSION are unchanged.
  • host_denied permits only when every reading from url_policy_hosts is permitted. It denies on error and logs the exception class only, so a credentialed mirror URL cannot reach the log.
  • It runs before each manifest request, before the poster and before the clip, and on every redirect step of the manifest fetch through redirect_allowed. That callback only narrows the existing redirect handlers.
  • Poster and clip downloads keep the same-host redirect handler, so a redirect cannot move them to another host.

Tests: feature-video and asset-downloader tests pass (530), and the new hosts test file passes. Mutations, one at a time: removing the poster check, the manifest check or the redirect guard; changing all-readings to any-reading; allowing on error; and dropping the percent-decoded reading each turned a named test red.

Non-blocking follow-up: removing the clip host check alone leaves the suite green. The poster check on the same host absorbs it, so the clip check is only exercised when the policy changes between poster and clip. A test that installs a hosts list after the poster downloads would pin it.

@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from d83c076 to 3e36d0f Compare October 24, 2026 20:22
@iamwhatever
iamwhatever enabled auto-merge (squash) October 24, 2026 20:22
@iamwhatever

Copy link
Copy Markdown
Contributor Author

Same change as #23716 (shared archetypes.py): a host list entry no reading can match (non-ASCII, root dot, non-canonical IP) now fails the policy load. Head 3e36d0f.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: passed Eligible automated validation passed for the current revision readiness: checking Automated validation is still running labels Oct 24, 2026
@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from 3e36d0f to 37c5351 Compare October 24, 2026 22:14
@iamwhatever

Copy link
Copy Markdown
Contributor Author

Rebased on main. Same validator change as #23716 (shared archetypes.py): a non-glob host entry must be exactly a form hosts are asked in, so zoned or bracketed IPv6, a port, %, / or @ now fail the policy load. Head 37c5351.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: passed Eligible automated validation passed for the current revision readiness: checking Automated validation is still running labels Oct 24, 2026
@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from 37c5351 to 12ec3f1 Compare October 24, 2026 23:54
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: passed Eligible automated validation passed for the current revision readiness: checking Automated validation is still running labels Oct 24, 2026
@iamwhatever
iamwhatever force-pushed the feat/ee-list-feature-video-hosts branch from 12ec3f1 to 05bd4d7 Compare October 25, 2026 00:54
@iamwhatever

Copy link
Copy Markdown
Contributor Author

Fixed the GPT/Opus BLOCK on the previous head (_unmatchable_glob refused valid DNS globs with numbered labels, canonical IPv6 globs such as 2001:db8:10:0:*, and _). Numbers are now checked only in IP-shaped globs, and the zero-run test compares groups. Shared archetypes.py: #23716 87e14b4, #23717 05bd4d7, #23719 937a1ef.

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Oct 25, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

readiness: checking Automated validation is still running

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants