Skip to content

refactor(public-safety): decide the credential question at the two capability faces - #5876

Open
Hsuehtan wants to merge 3 commits into
loopx-project:mainfrom
Hsuehtan:refactor/private-text-caller-face-policies
Open

Hsuehtan wants to merge 3 commits into
loopx-project:mainfrom
Hsuehtan:refactor/private-text-caller-face-policies

Conversation

@Hsuehtan

@Hsuehtan Hsuehtan commented Oct 7, 2026 •

Copy link
Copy Markdown

Goal And Delivered Outcome

  • Outcome basis / optional anchor: #5136 (open; the maintainer's four directions of 2026-09-27) and
    the successor note #5335 recorded when it merged — "the remaining caller faces … each need a
    named policy chosen from their destination".
  • Goal/source and gap: loopx/capabilities/decision_context/packets.py:50 and
    loopx/capabilities/material_lifecycle/_validation.py:25 each kept a credential alternation list,
    byte-identical to each other, consulted beside the owner's shape detector:
    SECRET_LIKE_SURFACE_PATTERN.search(text) or _CREDENTIAL_RE.search(text). The comment above both
    lists said "local threshold policy only", but the list holds labels and a header shape — a shape
    owner, not a threshold. So two capability faces independently decided which English spellings count
    as a credential, and a fix in the shared owner silently left both behind. Consumers affected: every
    Decision Context packet field and every Material Lifecycle validation field that goes through
    _compact_text / compact_text.
  • Observable before → after: a packet field that only mentions password, secret or api_key
    with no value adjacent is no longer refused; every assignment the old lists accepted still is, plus
    five spellings they missed and one spelling no owner arm reached before (db_password = "…").
    Proven by the unit rows (both faces driven through their real entrypoints) and the
    regression_parity row (named accepted/rejected lists recomputed over a 2,147-input grid).
  • Issue/task and intended base: Related to [Architecture]: two modules both claim to own "private-looking text" #5136; continues the caller migration fix(public-safety): separate credential words from credential values #5335 opened. Nothing
    is closed here — directions 1-4 of that issue remain open for the faces this slice does not move.

Author Declaration

  • Written by: model_agent (Claude, Anthropic), against this repository's main at aa87cc019.

Implemented against

  • Specification and revision: [Architecture]: two modules both claim to own "private-looking text" #5136 direction 1 ("the shared home is loopx/public_safe_text.py; a
    consumer consults it instead of keeping its own shapes", "one policy-bearing call, not an OR of
    detectors") and direction 2 ("a bare Bearer, password or secret mention, and prose such as
    'the Bearer token expired', is not a leak"), plus direction 4 ("report newly accepted / newly
    rejected per migrated face", "keep each surface's own wording and length limit"). Also the promise in
    tests/control_plane/test_public_safety_credential_shape_owner.py:8-12, which after refactor(public-safety): decide credential shapes in one owner #5135 states
    that "each site keeps only its own threshold policy … and consults the owner for the shapes".
Criterion (spec clause) Disposition Symbol / path Test or command
direction 1 — one home for the shapes implemented loopx/public_safe_text.py classify_private_text test_no_module_outside_the_owner_keeps_a_credential_alternation_list
direction 1 — one policy-bearing call, not an OR of detectors implemented _compact_text, compact_text test_both_faces_reject_a_credential_label_glued_into_a_field_name
direction 2 — a bare word is not a leak implemented, per face CATEGORY_CREDENTIAL_WORD, CREDENTIAL_CATEGORIES test_both_faces_accept_a_bare_mention_with_no_value_beside_it
direction 4 — newly accepted / newly rejected reported per face implemented NEWLY_ACCEPTED, NEWLY_REJECTED unit row below
direction 4 — surface keeps its own wording and limit implemented both faces still raise contains a credential-like value, max_len unchanged test_benign_prose_still_passes_both_faces
direction 2 — an explicit short-assignment policy out_of_scope LABELED_CREDENTIAL_ASSIGNMENT_PATTERN (named by #5335) not run — no decision made here
direction 3 — file:// / ~/ recognition at these faces out_of_scope find_public_safe_local_path still called unchanged not run — see Scope
direction 4 — Python/TS same-corpus parity not_applicable corpus and TS mirror untouched by this diff test_public_safe_text_owner_parity re-run, unchanged verdicts
  • Self-check before submission: read both faces' call sites, the owner's category table
    (_CATEGORIZED_PRIVATE_TEXT_PATTERNS, _SHAPE_DETECTORS) and every named policy
    (TEXT_OWNER_CATEGORIES, ARTIFACT_LIFECYCLE_CATEGORIES) before choosing the mechanism; measured the
    accepted/rejected delta by running the old union and the new policy over a generated grid rather than
    reasoning about it; deliberately left path and URL handling exactly as it was. What is assumed, not
    verified: the four migrated text owners' verdicts are held by the corpus they already pin, not
    re-derived here.

Scope And Continuation

  • Completed scope and remaining work:
    • Done: both private lists deleted (and import re removed from both files, where the list was the
      only user); one categorized call per face against the named CREDENTIAL_CATEGORIES; a new owner
      arm COMPOUND_CREDENTIAL_FIELD_ASSIGNMENT_PATTERN for the one spelling the lists reached and every
      category arm misses — a credential label glued into a field name, invisible because the label arms
      anchor with \b and _ is a word character; a census guard that names any module still deciding
      this question for itself.
    • Deliberately not decided here:
      1. remote_location and local_path at these two faces. Each still asks
        find_public_safe_local_path and REMOTE_LOCATION_SURFACE_PATTERN separately, unchanged.
        Folding them into the same call would let this slice decide, per face, whether internal-state
        text may carry a URL — the caller migration [Architecture]: two modules both claim to own "private-looking text" #5136 is still open for.
      2. The short-assignment decision. Direction 2 asks for an explicit, tested policy and does not
        say which way; the arm has been named since fix(public-safety): separate credential words from credential values #5335 and is untouched.
      3. Three faces the new census found, declared rather than converted. extensions/presentation.py
        rejects an assignment whose value is one character, which no owner arm reproduces without the
        undecided item above; control_plane/todos/handoff_note.py also names vendor forms
        (ak/sk, access_key_id) the owner does not; loopx/contract.py is the publication tier,
        whose move needs the corpus parity slice rather than two capability tables. Each is declared in
        DECLARED_OPEN_SITES with its reason, and a declaration whose site has already been converted
        fails (M9 in the mutation table).
    • Three further credential-ish rules exist and are not flagged, on purpose:
      benchmark_toolkit/environment_access.py (environment-variable names),
      control_plane/testing/model_behavior_qualification.py (field names) and registry.py's private
      text marker list. They decide a name or a marker, not whether free text carries a credential value;
      the criterion and its limit are stated in _is_credential_text_rule and pinned by probe rows.
  • Slice boundary / successor: independently reversible — reverting restores two constants, two call
    sites and one owner arm without touching the corpus, the TS mirror or another face. Successor: the
    three declared faces, then the direction-2 short-assignment decision that unblocks the strictest one.

Validation

  • Tested revision: a875f6cdf (3 commits, 4 files, +449 −39); 621b1229b product, a3ef532c3 census
    fold fix, a875f6cdf formatting.
  • Run state: finished
  • Input classes: public_fixture, synthetic
Check kind Result Public-safe evidence / limitation
unit passed python -m pytest -q tests/control_plane/test_public_safety_credential_caller_faces.py -> 61 passed: per-face rejection of the glued-field class, the pinned newly-accepted / newly-rejected lists, the arm's load-bearing check, the census and its five probe rows (both faces driven through _compact_text / compact_text, with an unpatched positive control).
unit passed python -m pytest -q tests/control_plane -k "public_safety or public_safe or decision_context or material_lifecycle or maintainability" -> 604 passed / 0 failed, 6930 deselected: the owner, the classifier's 4,032-form biconditional, the cross-runtime corpus, the four migrated text owners, and both faces' own suites.
integration passed (net zero) python -m pytest -q tests/architecture tests/canary — head 2 failed / 1444 passed, and the same selection in an unmodified worktree at the base revision 2 failed / 1444 passed, with the two failure ids identical in both (test_top_level_module_budget… = the 148-vs-147 pin reported in #5685, test_source_session_registry_denial…). Both runs used the same interpreter and the repository's qualified Node line.
static passed python -m ruff check clean; python -m mypy -> Success: no issues found in 19 source files; loopx check --scan-path on all four changed paths -> errors=0.
regression_parity passed Old-union vs new-policy over a 2,147-input grid (labels × separators × values, plus the shared corpus and prose rows): 1,447 rejects before, 886 after. Newly rejected 180, by owner reason — assignment shape 76, authorization shape 48, label-carrying-shaped-value 39, bearer word 16, basic-auth value 1. Newly accepted 741, all of them a label with nothing adjacent stating an assignment: 266 rows where a value is glued straight onto the label with no operator or space (100 of those carry a token-shaped run, e.g. a vendor prefix written after the word with no separator), and 4 dash-less PEM header spellings (BEGIN RSA PRIVATE KEY without its --- fence, which the old substring list caught and the owner's arm requires the fence for). Every row that pairs a label with an operator still rejects. Both lists are pinned as named assertions, so a future re-widening fails a test. Mutations: 10 variants, 9 killed — a face re-adding a private list (1 red), each face dropping the opt-in (7 red each), the new arm anchored (9), the policy dropping credential_word (3), the arm applied regardless of the opt-in (3, including the classifier's biconditional), a face stop consulting the owner (35), the word arms re-widened to substrings (5), a declared site converted without retiring its declaration (1). 1 variant survives, disclosed as the census's stated limit: an alternation constructed inside a function body rather than at module level, which the fold does not see; a fifth row pins that a word list with no whitespace test is likewise not caught.
census passed (net zero) python examples/semantic-vocabulary-drift-smoke.py measured on this head and on an unmodified worktree at the base revision, byte-for-byte the same line: same_runtime_forks=2/2 same_runtime_fork_definitions=5/5 conflicting_values=16/16 conflicting_definitions=54/54 schema_version_same_runtime_forks=0/0 multi_value_twins=8/8 multi_value_forks=2/2 multi_value_forks_semantic=1/1 multi_value_fork_definitions=6/6 same_runtime_forks_semantic=2/2 conflicting_values_semantic=0/0, twins_raw=45, generated_verified=2, independently_maintained=43/43. No registry budget and no BUDGET_ANCHOR literal moves in this PR, and the registry I/O census test inside the compared architecture selection reports the same outcome on both sides.
manual passed Disclosure, not evidence: ruff format --check is not a gate here, and all three product files already fail it on the unmodified base — loopx/public_safe_text.py 5 hunks before / 5 after, packets.py 2 / 2, _validation.py 1 / 1. The new test file was formatted and reports 0 hunks.
  • Coverage and gaps: the two migrated faces are covered through their real entrypoints rather than the
    patterns, so a face that quietly stops consulting the owner goes red (mutation M7 confirms: 35). The
    new arm is covered by 7 pinned spellings × both faces, and by a with/without the flag comparison that
    fails if the arm ever stops being the only reason that class rejects. Gaps named plainly: (a) the
    local-path and remote-location questions at these faces are unchanged and unmeasured here; (b) the
    three declared faces keep their own verdicts, so "one owner for credential text" is still a program
    rather than a fact after this PR; (c) the fold covers literal concatenation, "|".join([...]) and
    module-level name chains — an f-string or a function-built pattern is not folded, which is the
    surviving mutation; (d) no TS surface is touched, so the two runtimes' behaviour for these two faces
    is not a claim here (they are not mirrored in the Vision checkpoint path).

See validation disclosure guidance.

Frontend / Visual Evidence

  • UI impact: none
  • Before: N/A
  • After: N/A
  • States and viewports shown: N/A
  • Source data: none
  • Attention review: N/A

Type of Change

  • Bug fix — the two faces refused prose that names a credential word, which [Architecture]: two modules both claim to own "private-looking text" #5136 direction 2
    states is not a leak
  • New feature
  • Breaking change
  • Refactoring (no functional changes) — not checked on purpose: verdicts change in both
    directions, quantified in the regression_parity row
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)

Technical Direction

Shared-authority RFC fixture impact

N/A — no TypeScript control-plane migration or shared Goal Authority claim. tests/fixtures/
public_safe_text_corpus.json, the TS Vision mirror and the dual-runtime twin inventory are untouched
by this diff; the parity suite was re-run and reports the same verdicts as before.

decision_context packets and material_lifecycle validation each kept their own
credential alternation list beside the owner's shape detector, byte-identical to
each other, under a comment that called it a "local threshold policy". A list of
labels and header shapes is a shape owner, not a threshold.

Both faces now make one categorized call to the shared text owner with the named
CREDENTIAL_CATEGORIES policy. The owner gains
COMPOUND_CREDENTIAL_FIELD_ASSIGNMENT_PATTERN, reached only through the new
include_compound_field_assignment opt-in: it is the one spelling the private lists
caught and every category arm misses, because those arms anchor the label with a
word boundary and underscore is a word character. The opt-in stays off by default
so the four migrated text owners and the publication tier change no verdict.

Refs loopx-project#5136 directions 1 and 2, and the caller-facing successor loopx-project#5335 recorded.

Signed-off-by: Hsuehtan <296098438+Hsuehtan@users.noreply.github.com>
…e call

The first version folded the assignment value, which for a construction is the
re.compile(...) call rather than its pattern, so the census reported a clean
repository even with the deleted lists put back. It also keyed on any three labels,
which flagged environment-name and field-name rules that decide a different
question. The criterion is now an alternation that reaches for whitespace, three
sites that still decide it are declared with the reason each one stays, and the
probe rows state the limit out loud.

Signed-off-by: Hsuehtan <296098438+Hsuehtan@users.noreply.github.com>
ruff format --check is not a gate in this repository, but a file added here should
not add a new violation, so this one is formatted and reports no hunks.

Signed-off-by: Hsuehtan <296098438+Hsuehtan@users.noreply.github.com>

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant