Releases: ModelW/wtf
Releases · ModelW/wtf
Release list
v1.2.3
v1.2.2
data rights. A human records an exemption (--exempt legal_obligation --note "..."), a declared gap (--missing --note) or clears an entry on
one right of one personal item (or aModel.*glob) from the command
line; until now only the agents'data_flagtool could write the
rightsblock.
v1.2.1
- Rights on a JSON content are read, not orphaned. A reviewer's
data_flagon<field>@json.<name>(a retention gap observed on one key
of a blob) wrote anoverriderow whose id names no column;check
then reported it as a manual item lacking a description — an error the
agents had no way to clear. The row is now the content's rights block:
it lands on the declared content, needs the column'scontents
declaration (data-orphanotherwise), must name a declared content
(data-ref-unknown) and a personal one (rights-on-non-personal). app set/app showandactivities set. The!todoquestions
of the product row (description,large_scale, controller, processor)
and of an activity (legal basis, consent record, interest, retention,
data subjects, recipients...) are answered from the command line, like
parties set;--todo FIELDreopens one,--clear FIELDdrops an
optional one. No more editing the database by hand.- Docs: the add-to-project guide no longer says "edit the YAML" and
explains that activity grouping is the second pass oftouchpoints auto-review(--group), not a command of its own.
v1.2.0
- The gate installs the base's environment when the lock differs. A
pull request that changesuv.lock/pnpm-lock.yamlused to leave the
base worktree without a.venv/node_modules;uv run --no-sync
then created an empty venv and introspection died with
ModuleNotFoundError: No module named 'django'— a tool error that
failed the whole gate. The base's environment is now built from its own
lock (uv sync --frozen --no-dev,pnpm install --frozen-lockfile,
...) inside the worktree; only when that is impossible does the base
run approximate, with a warning. - Storage moves from YAML to SQLite. Every declaration — the app, the
parties, data overrides and reviews, touchpoint declarations, stores,
activities, threat stamps, the findings register, custom vocabularies
and actors — lives in onecompliance.dbat the repository root; the
compliance/folders are gone andsnow.yml'scompliance:block only
names the discovery engine (diris dropped). The database is tuned to
diff well (WAL, no auto-vacuum, a checkpoint on close) andinitadds
its transient files to.gitignore. Cross-references (hosts, data refs,
transfers, recipients, stamps, locks) are normalised tables queried with
SQL rather than loaded and joined in Python. No migration from the YAML
layout: it is a new system. - The
CODEOWNERSstep ofinit(and itscheckdiagnostic and flags)
is removed. - Marker subjects are
<record>#<field>(parties/acme#address,
activities/ordering#retention,app#description) instead of file
names. - The library takes the repository from a process-wide container set
once per command (--root); noroot=/shared=arguments. - Duplicate parties are refused.
party_add(andinit) compare the
new party with every declared one — normalised name (case, accents,
punctuation, legal forms and a typo or an extra word ignored), shared
registrable domain or setting name of the website /hosts, respelled
id — and fail with the matching id when one looks the same. A party
that names a store of the project (Sentry, "SMTP mail server", a
host ofEMAIL_HOST) is refused too: that is a store write. The
refusal is final for agents;distinct_fromis set by a human only.
checkreports coexisting lookalikes as aparty-duplicateerror. - Duplicate stores are refused the same way:
store_addcompares the
new store with the unit's visible ones (shared host / settings name,
alike name, slug or config backend) and fails with the slug to reuse;
checkreports coexisting lookalikes asstore-duplicate. partiesCLI:list,show,add,set,remove [--force],
merge ... --into,to-store <id> <unit:slug>(a party that was
infrastructure becomes store writes),to-flow <id>(a party that was
the project itself: its transfers go, the derived call stays),
distinct,duplicates.- Reach: who the code lets in, apart from who it serves. A finding's
likelihood used to come from the touchpoint'sscope, so asystem
webhook listing whoseget_permissions()returned[]on the leader —
readable by anyone on the internet — scoredinfo(system, 0.01)
instead ofhigh(anonymous). Touchpoints now carry areach
(anonymous | subject | staff | system), inferred from the auth facts
alone (no auth is anonymous whatever the scope says) and declared by the
reviewer throughtouchpoint_set_data(reach=). The Django introspection
reportsauth_custom— DRF views overridingget_permissions,
get_authenticators,dispatch… or using a hand-written authentication /
permission class — and such a touchpoint is pending until a reviewer
reads the override and declares its reach; the declaration is refused
without it. Schema v4 addstouchpoints.reach. - Severity buckets rebalanced.
criticalis an open buffet — anyone
(or any account onto every other account's data) takes personal data in
bulk, a confidential listing or a special-category record;highis
what takes some work, or an account, to get at what is not yours;
mediumis what someone could do that they should not. Concretely: the
bulkdegree weighs 2 (each degree doubles the last), critical starts
at 4, high at 2, medium at 1; every reviewable threat in_mapping.yaml
declares aneffort(openwalk in ×1,worka script or payload ×¾,
chainanother flaw or a victim ×½) that scales the likelihood, so a
brute force on a login or a stored XSS ishigh, notcritical; the
ownership threats (AA03, AC01, AC07, AC12, DS05) arehorizontal— a
subject who can read every other subject's rows weighs like an anonymous
caller; the oracle threats (DS01, INP18) are capped at oneattribute
and fingerprinting (DS03, HA03) atexistence, whatever the touchpoint
lists; escalation weighs like a disclosure of the data the touchpoint
handles (floor 2) instead of a flat 4, so an unauthenticated webhook that
creates one record ishighand an unauthenticated confidential listing
critical; a tampering on a touchpoint that only creates is one
record; denial of service is a flat 1 whatever the data behind the
endpoint (an outage is an incident, not a breach), so an unthrottled
public endpoint ismedium, nothigh. A touchpoint's write into an internal store of the project
(own_store_write: database, files, cache, queue, search, realtime) no
longer carries the leak cells DS06/DR01 — the response to the caller and
the mail / monitoring / external flows keep them. - Reach is derived through the front. A SvelteKit route with no auth
of its own takes the reach of the api touchpoints itcalls(the
cookie is forwarded: whoever the api lets in) and of a parent
+layout.server.tsthat redirects unauthenticated callers to login (the
introspection reports such aloadas alogin guardauth fact). The
api.bizneo(fetch).opId()client-factory call style is now recognised,
and DRF routes carry drf-spectacular's operation ids, so front routes
link to the DRF endpoints they proxy. auto-reviewnarrates each write once: the line comes from the activity
log the MCP server appends to, no longer also from the tool-call stream
(activities, parties and declarations were printed twice). Stores,
undeclared-flow reports and challenges get a line too.- A reported sink that names a store the way the party guard sees it
(sdk-sentry-sdk, "Sentry error tracker", "the mail server") resolves
to that store, so declaring the store write closes the report. Before,
the sink stayed an unresolvable transfer:party_addrefused the party
(it IS the store) and the touchpoint could never leave pending. - A relative
fetch("/api/x")in a SvelteKit route is linked to the
route of the project that serves it (acallsedge, like a generated
client call) and leaves the outboundfetches: the project's own
endpoints never show up as an unknown host to declare a party for.
storesgainsadd,set,remove,merge,distinct. - Outgoing email and error monitoring are built-in stores. Every
Django unit has amail-defaultstore (typemail) claiming
EMAIL_BACKEND/EMAIL_HOST, and anerrors-sentrystore (type
monitoring) claimingsentry_sdk/SENTRY_DSNwherever the SDK is
installed; asend_mail/EmailMessage/email_usercall, or a
capture_exception, is a write to them. How mail is sent and where
events land (cloud, self-hosted) is infrastructure, so no distinction is
made between backends: no moresmtp-*stores or Sentry / mail provider
parties declared by agents. load_declarationsreads the parties even when theapprow is
missing, soparties listworks beforeinit.- Fix: the unit's introspection no longer inherits
VIRTUAL_ENV/
PYTHONPATH/ uv and poetry variables from the process running
model-wtf. Underuv run model-wtf,poetry runin the unit honoured
ourVIRTUAL_ENVand imported the project in model-wtf's venv
(No module named 'celery'), anduv runcreated a stray.venvand
uv.lockin the unit. - Schema migrations:
compliance.dbcarries its schema version and is
upgraded step by step (model_wtf.compliance.migrations) when a newer
release opens it; a file from a newer release is refused.
v1.2.0rc2
- Storage moves from YAML to SQLite. Every declaration — the app, the
parties, data overrides and reviews, touchpoint declarations, stores,
activities, threat stamps, the findings register, custom vocabularies
and actors — lives in onecompliance.dbat the repository root; the
compliance/folders are gone andsnow.yml'scompliance:block only
names the discovery engine (diris dropped). The database is tuned to
diff well (WAL, no auto-vacuum, a checkpoint on close) andinitadds
its transient files to.gitignore. Cross-references (hosts, data refs,
transfers, recipients, stamps, locks) are normalised tables queried with
SQL rather than loaded and joined in Python. No migration from the YAML
layout: it is a new system. - The
CODEOWNERSstep ofinit(and itscheckdiagnostic and flags)
is removed. - Marker subjects are
<record>#<field>(parties/acme#address,
activities/ordering#retention,app#description) instead of file
names. - The library takes the repository from a process-wide container set
once per command (--root); noroot=/shared=arguments. - Duplicate parties are refused.
party_add(andinit) compare the
new party with every declared one — normalised name (case, accents,
punctuation, legal forms and a typo or an extra word ignored), shared
registrable domain or setting name of the website /hosts, respelled
id — and fail with the matching id when one looks the same. A party
that names a store of the project (Sentry, "SMTP mail server", a
host ofEMAIL_HOST) is refused too: that is a store write. The
refusal is final for agents;distinct_fromis set by a human only.
checkreports coexisting lookalikes as aparty-duplicateerror. - Duplicate stores are refused the same way:
store_addcompares the
new store with the unit's visible ones (shared host / settings name,
alike name, slug or config backend) and fails with the slug to reuse;
checkreports coexisting lookalikes asstore-duplicate. partiesCLI:list,show,add,set,remove [--force],
merge ... --into,to-store <id> <unit:slug>(a party that was
infrastructure becomes store writes),to-flow <id>(a party that was
the project itself: its transfers go, the derived call stays),
distinct,duplicates.- Reach: who the code lets in, apart from who it serves. A finding's
likelihood used to come from the touchpoint'sscope, so asystem
webhook listing whoseget_permissions()returned[]on the leader —
readable by anyone on the internet — scoredinfo(system, 0.01)
instead ofhigh(anonymous). Touchpoints now carry areach
(anonymous | subject | staff | system), inferred from the auth facts
alone (no auth is anonymous whatever the scope says) and declared by the
reviewer throughtouchpoint_set_data(reach=). The Django introspection
reportsauth_custom— DRF views overridingget_permissions,
get_authenticators,dispatch… or using a hand-written authentication /
permission class — and such a touchpoint is pending until a reviewer
reads the override and declares its reach; the declaration is refused
without it. Schema v4 addstouchpoints.reach. - Severity buckets rebalanced.
criticalis an open buffet — anyone
(or any account onto every other account's data) takes personal data in
bulk, a confidential listing or a special-category record;highis
what takes some work, or an account, to get at what is not yours;
mediumis what someone could do that they should not. Concretely: the
bulkdegree weighs 2 (each degree doubles the last), critical starts
at 4, high at 2, medium at 1; every reviewable threat in_mapping.yaml
declares aneffort(openwalk in ×1,worka script or payload ×¾,
chainanother flaw or a victim ×½) that scales the likelihood, so a
brute force on a login or a stored XSS ishigh, notcritical; the
ownership threats (AA03, AC01, AC07, AC12, DS05) arehorizontal— a
subject who can read every other subject's rows weighs like an anonymous
caller; the oracle threats (DS01, INP18) are capped at oneattribute
and fingerprinting (DS03, HA03) atexistence, whatever the touchpoint
lists; escalation weighs like a disclosure of the data the touchpoint
handles (floor 2) instead of a flat 4, so an unauthenticated webhook that
creates one record ishighand an unauthenticated confidential listing
critical; a tampering on a touchpoint that only creates is one
record; denial of service is a flat 1 whatever the data behind the
endpoint (an outage is an incident, not a breach), so an unthrottled
public endpoint ismedium, nothigh. A touchpoint's write into an internal store of the project
(own_store_write: database, files, cache, queue, search, realtime) no
longer carries the leak cells DS06/DR01 — the response to the caller and
the mail / monitoring / external flows keep them. - Reach is derived through the front. A SvelteKit route with no auth
of its own takes the reach of the api touchpoints itcalls(the
cookie is forwarded: whoever the api lets in) and of a parent
+layout.server.tsthat redirects unauthenticated callers to login (the
introspection reports such aloadas alogin guardauth fact). The
api.bizneo(fetch).opId()client-factory call style is now recognised,
and DRF routes carry drf-spectacular's operation ids, so front routes
link to the DRF endpoints they proxy. auto-reviewnarrates each write once: the line comes from the activity
log the MCP server appends to, no longer also from the tool-call stream
(activities, parties and declarations were printed twice). Stores,
undeclared-flow reports and challenges get a line too.- A reported sink that names a store the way the party guard sees it
(sdk-sentry-sdk, "Sentry error tracker", "the mail server") resolves
to that store, so declaring the store write closes the report. Before,
the sink stayed an unresolvable transfer:party_addrefused the party
(it IS the store) and the touchpoint could never leave pending. - A relative
fetch("/api/x")in a SvelteKit route is linked to the
route of the project that serves it (acallsedge, like a generated
client call) and leaves the outboundfetches: the project's own
endpoints never show up as an unknown host to declare a party for.
storesgainsadd,set,remove,merge,distinct. - Outgoing email and error monitoring are built-in stores. Every
Django unit has amail-defaultstore (typemail) claiming
EMAIL_BACKEND/EMAIL_HOST, and anerrors-sentrystore (type
monitoring) claimingsentry_sdk/SENTRY_DSNwherever the SDK is
installed; asend_mail/EmailMessage/email_usercall, or a
capture_exception, is a write to them. How mail is sent and where
events land (cloud, self-hosted) is infrastructure, so no distinction is
made between backends: no moresmtp-*stores or Sentry / mail provider
parties declared by agents. load_declarationsreads the parties even when theapprow is
missing, soparties listworks beforeinit.- Fix: the unit's introspection no longer inherits
VIRTUAL_ENV/
PYTHONPATH/ uv and poetry variables from the process running
model-wtf. Underuv run model-wtf,poetry runin the unit honoured
ourVIRTUAL_ENVand imported the project in model-wtf's venv
(No module named 'celery'), anduv runcreated a stray.venvand
uv.lockin the unit. - Schema migrations:
compliance.dbcarries its schema version and is
upgraded step by step (model_wtf.compliance.migrations) when a newer
release opens it; a file from a newer release is refused.
v1.2.0rc1
- Storage moves from YAML to SQLite. Every declaration — the app, the
parties, data overrides and reviews, touchpoint declarations, stores,
activities, threat stamps, the findings register, custom vocabularies
and actors — lives in onecompliance.dbat the repository root; the
compliance/folders are gone andsnow.yml'scompliance:block only
names the discovery engine (diris dropped). The database is tuned to
diff well (WAL, no auto-vacuum, a checkpoint on close) andinitadds
its transient files to.gitignore. Cross-references (hosts, data refs,
transfers, recipients, stamps, locks) are normalised tables queried with
SQL rather than loaded and joined in Python. No migration from the YAML
layout: it is a new system. - The
CODEOWNERSstep ofinit(and itscheckdiagnostic and flags)
is removed. - Marker subjects are
<record>#<field>(parties/acme#address,
activities/ordering#retention,app#description) instead of file
names. - The library takes the repository from a process-wide container set
once per command (--root); noroot=/shared=arguments. - Duplicate parties are refused.
party_add(andinit) compare the
new party with every declared one — normalised name (case, accents,
punctuation, legal forms and a typo or an extra word ignored), shared
registrable domain or setting name of the website /hosts, respelled
id — and fail with the matching id when one looks the same. A party
that names a store of the project (Sentry, "SMTP mail server", a
host ofEMAIL_HOST) is refused too: that is a store write. The
refusal is final for agents;distinct_fromis set by a human only.
checkreports coexisting lookalikes as aparty-duplicateerror. - Duplicate stores are refused the same way:
store_addcompares the
new store with the unit's visible ones (shared host / settings name,
alike name, slug or config backend) and fails with the slug to reuse;
checkreports coexisting lookalikes asstore-duplicate. partiesCLI:list,show,add,set,remove [--force],
merge ... --into,to-store <id> <unit:slug>(a party that was
infrastructure becomes store writes),to-flow <id>(a party that was
the project itself: its transfers go, the derived call stays),
distinct,duplicates.auto-reviewnarrates each write once: the line comes from the activity
log the MCP server appends to, no longer also from the tool-call stream
(activities, parties and declarations were printed twice). Stores,
undeclared-flow reports and challenges get a line too.- A reported sink that names a store the way the party guard sees it
(sdk-sentry-sdk, "Sentry error tracker", "the mail server") resolves
to that store, so declaring the store write closes the report. Before,
the sink stayed an unresolvable transfer:party_addrefused the party
(it IS the store) and the touchpoint could never leave pending. - A relative
fetch("/api/x")in a SvelteKit route is linked to the
route of the project that serves it (acallsedge, like a generated
client call) and leaves the outboundfetches: the project's own
endpoints never show up as an unknown host to declare a party for.
storesgainsadd,set,remove,merge,distinct. - Outgoing email and error monitoring are built-in stores. Every
Django unit has amail-defaultstore (typemail) claiming
EMAIL_BACKEND/EMAIL_HOST, and anerrors-sentrystore (type
monitoring) claimingsentry_sdk/SENTRY_DSNwherever the SDK is
installed; asend_mail/EmailMessage/email_usercall, or a
capture_exception, is a write to them. How mail is sent and where
events land (cloud, self-hosted) is infrastructure, so no distinction is
made between backends: no moresmtp-*stores or Sentry / mail provider
parties declared by agents. load_declarationsreads the parties even when theapprow is
missing, soparties listworks beforeinit.- Fix: the unit's introspection no longer inherits
VIRTUAL_ENV/
PYTHONPATH/ uv and poetry variables from the process running
model-wtf. Underuv run model-wtf,poetry runin the unit honoured
ourVIRTUAL_ENVand imported the project in model-wtf's venv
(No module named 'celery'), anduv runcreated a stray.venvand
uv.lockin the unit. - Schema migrations:
compliance.dbcarries its schema version and is
upgraded step by step (model_wtf.compliance.migrations) when a newer
release opens it; a file from a newer release is refused.
v1.0.0rc1
The first release. compliance init scaffolds a repository; the four
inventories (data, touchpoints, flows, stores) are introspected from the
code and reviewed by agents or humans; activities, rights coverage and the
threat model are derived; check is the to-do list and ghate the gate on
every pull request, with a challenger agent that re-opens the reviews a
change undermines.
- Data inventory from Django models with rules, reviews, JSON contents and
overrides (KFF-194–199). - Touchpoints from Django URL confs, task registries and SvelteKit trees;
declarations with ops, scope, transfers and store writes (stores:for
the project's own second-tier stores, declared withhosts); activities and legal bases;
rights coverage derived from ops × scope (KFF-200–208). - The gate:
ghatecompares base and head by finding identity; the
challenger re-opens data items, touchpoint declarations and threat stamps
from the diff (KFF-205, KFF-210, KFF-217). - Threats: pytm's 114 threats mapped, deterministic dismissal rules, a
matrix over touchpoints, stores, parties and flows, stamps in the
manifests, severity from effect × degree × sensitivity × actor, stable
finding ids, a per-topic reviewer swarm (KFF-211–215). - Flows as an inventory with kinds and statuses; undeclared transfers by
construction from the hosts the code calls (KFF-216). CODEOWNERSentries at init (KFF-204); the documentation site
(KFF-218); this release machinery (KFF-219).