Skip to content

2404 stack 4 model definition accuracy - #2447

Merged
correct-horse-battery-bench merged 11 commits into
2404-stack-3-document-metadata-and-pathsfrom
2404-stack-4-model-definition-accuracy
Aug 21, 2026
Merged

2404 stack 4 model definition accuracy#2447
correct-horse-battery-bench merged 11 commits into
2404-stack-3-document-metadata-and-pathsfrom
2404-stack-4-model-definition-accuracy

Conversation

@correct-horse-battery-bench

@correct-horse-battery-bench correct-horse-battery-bench commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Update model definitions so the openapi spec becomes more accurate.

  • make sure post fields are required as well as the same model is used for get and post
    • has the effect that create needs to pass some fields which have defaults but this makes the openapi spec more accurate for gets

@correct-horse-battery-bench
correct-horse-battery-bench force-pushed the 2404-stack-4-model-definition-accuracy branch from 08ed599 to 0d32bf3 Compare August 18, 2026 07:44
@correct-horse-battery-bench
correct-horse-battery-bench force-pushed the 2404-stack-4-model-definition-accuracy branch from 07a3496 to 9f3543b Compare August 18, 2026 14:07
Every optional key of a column definition was defaulted where it was read, so
"subtype", "protected", "private", "alias", "public" and "dba_mapping" each
repeated an array_key_exists check inside the string that builds the feature
dict, and the nullability of a column was spelled as a double negation twice.

Merge the defaults onto the column once before the loop, so the feature dict is
built from keys that are known to be there, and state the rule the double
negation encoded: a column without a "null" key is nullable, "null" => False
makes it non-nullable. A choice key may be a string as well as an integer, which
the emitted choice list now quotes accordingly.

The generated models are unchanged; running the generator produces the same
files.
Twelve columns hold one of a fixed set of values, decided by a define in
src/inc/defines/ or by the notification registry, and said so nowhere: the spec
described them as a plain integer or string, so a client had to read the server
source to learn that an agent runs on one of three operating systems, and the
API accepted any number for them.

Attach the choices to the columns that have them, taken from the define that
decides the value: the operating system of an agent, the type of an agent
statistic, the state of a chunk, the type of a file, the status of a file
download, the status and mode of a health check, the status of a health check
agent, the issuer and level of a log entry, and the two columns of a
notification setting.

The two notification columns are the ones the spec got wrong until now. 'action'
names the event that triggers a notification, which NotificationUtils validates
against DNotificationType, and 'notification' names the delivery method, which
it validates against the registered HashtopolisNotification instances. The
choices of the second are derived from that registry rather than listed a second
time, so a newly registered delivery method appears in the spec by being
registered.

Each choice becomes a oneOf branch of const and title in the spec, which is how
a generated client learns the name of a value, and validateData rejects a value
that is not one of them.
Eight columns disagreed with the schema they are stored in, so the spec promised
an attribute that can be missing and the API rejected a value the database
accepts, or the other way round.

Four are nullable in the database and were declared as never null: the chunk of
an agent error (an error is not necessarily tied to a chunk), the chunk of a
cracked hash in both hash tables, and the object a notification setting refers to
(an agent-wide event refers to none). The spec described them as an integer, so a
client following it broke on the null the API answers with, and PATCHing the
attribute back to null was refused.

Four are NOT NULL in the database and were declared as nullable: the benchmark of
an assignment, the revoked flag of an API key, the permission map of a permission
group and the valid flag of a user. Those become required on create, which is
what the database enforces anyway, and stop accepting a PATCH to null.

See src/migrations/*/20260619090219_initial.sql for the column definitions the
declarations now match.
TaskUtils::getStatus() and TaskWrapperDisplayAPI::getAggregateStatus() decided
the status of a task with bare numbers, explained by a comment that listed three
of the four values the functions return.

DTaskStatus names the five values, and both functions use the constants. The
values are unchanged; what the reader gains is that "skipped" is visible as such
at the two places that return it, for an archived task and for a task that found
no crack. The define is also what the generated spec will name the values of the
computed status attribute with, so the code producing them and the document
describing them stay in step.
Every config item was described as a plain string, although the value an item
takes is decided by its type: a select item accepts exactly the keys of its
selection, and DConfig knows both. A client editing a config had no way to learn
which values are valid other than trying one.

ConfigAPI answers the attributes schema of its resource itself, through
getOpenAPIAttributesSchemaOverride(): a hook this adds to AbstractBaseAPI for an
attributes object no feature type can describe, which the generator reads in
place of the schema it derives. The override carries one branch per select item,
naming the item as a const and its selection as a oneOf of const and title, plus
one generic branch for every item whose value is a free string.

The branches are titled after the item, so a generated client names the union
members after the config they belong to.
The benchmark column is NOT NULL, so declaring it as such made it mandatory on
POST /ui/agentassignments. The endpoint accepted the attribute and then dropped
it: AgentUtils::assign() overwrote whatever came in with a hardcoded 0, so the
spec demanded a value the API refused to keep.

Let assign() take the benchmark to record and hand it the value the request
carried. The other five callers - unassigning an agent, the assignAgent helper,
the two user_api entry points and the legacy agent handler - know no benchmark
and keep the "0" the function used to hardcode, as its default.
The isRevoked column is NOT NULL, so declaring it as such made it mandatory on
POST /ui/apiTokens. The endpoint accepted the attribute and then dropped it:
JwtTokenUtils::createKey() wrote a hardcoded 0, so a client that asked for a
revoked key got a live one back and the spec demanded a value the API refused
to keep.

Let createKey() take the flag and hand it the value the request carried. The
default is the "not revoked" the function used to hardcode, which is also the
default the column carries.
isValid is NOT NULL, so declaring it as such made it mandatory on POST
/ui/users, next to the sessionLifetime that was mandatory already. Both were
hidden from the spec: getAllPostParameters() unset them, so the create body
documented three attributes while the API insisted on five and answered any
request that followed the document with 400.

Drop the override and let the two attributes appear as the mandatory attributes
they are. createUser() has received both from the request all along, so the
"?? false" and "?? 3600" fallbacks stood for a case validateParameters() does
not let through and go with it.
@correct-horse-battery-bench
correct-horse-battery-bench force-pushed the 2404-stack-4-model-definition-accuracy branch from 9f3543b to be22079 Compare August 18, 2026 14:37
@correct-horse-battery-bench
correct-horse-battery-bench marked this pull request as ready for review August 18, 2026 14:38
Comment thread src/dba/models/NotificationSetting.php Outdated
$dict['notification'] = ['read_only' => False, "type" => "str(50)", "subtype" => "unset", "choices" => "unset", "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "notification", "public" => False, "dba_mapping" => False];
$dict['action'] = ['read_only' => False, "type" => "str(50)", "subtype" => "unset", "choices" => ["taskComplete" => "Task completed", "agentError" => "Agent error", "ownAgentError" => "Own agent error", "logError" => "Log error", "newTask" => "New task", "newHashlist" => "New hashlist", "hashlistAllCracked" => "Hashlist all cracked", "hashlistCrackedHash" => "Hashlist cracked hash", "userCreated" => "User created", "userDeleted" => "User deleted", "userLoginFailed" => "User login failed", "logWarn" => "Log warning", "logFatal" => "Log fatal", "newAgent" => "New agent", "deleteTask" => "Delete task", "deleteHashlist" => "Delete hashlist", "deleteAgent" => "Delete agent", ], "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "action", "public" => False, "dba_mapping" => False];
$dict['objectId'] = ['read_only' => True, "type" => "int", "subtype" => "unset", "choices" => "unset", "null" => True, "pk" => False, "protected" => True, "private" => False, "alias" => "objectId", "public" => False, "dba_mapping" => False];
$dict['notification'] = ['read_only' => False, "type" => "str(50)", "subtype" => "unset", "choices" => ["ChatBot" => "ChatBot", "Discord Webhook" => "Discord Webhook", "Email" => "Email", "Example" => "Example", "Slack" => "Slack", "Telegram" => "Telegram", ], "null" => False, "pk" => False, "protected" => False, "private" => False, "alias" => "notification", "public" => False, "dba_mapping" => False];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Baking the existing Notifications that strongly into the model is probably not ideal. Users can put in their own notification classes and use them. With this change, this would require to change DBA model files

Comment thread src/inc/utils/AgentUtils.php Outdated
* @throws Exception
*/
public static function assign(int $agentId, int $taskId, User $user): ?Assignment {
public static function assign(int $agentId, int $taskId, User $user, string $benchmark = "0"): ?Assignment {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it would be better to defaul to empty string than 0.

@correct-horse-battery-bench
correct-horse-battery-bench merged commit 3368833 into master Aug 21, 2026
24 checks passed
@correct-horse-battery-bench
correct-horse-battery-bench deleted the 2404-stack-4-model-definition-accuracy branch August 21, 2026 09:49
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.

2 participants