add typed IPC command-schema framework (T-120)
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 23s

Per D-74: commands register an argument schema beside their handler
instead of hand-validating args inline. DaemonDispatcher accumulates a
cmd->schema registry and, before invoking a handler, normalises the
argv-translator shape ({positional, flags}) into named args, coerces
types, and checks per-arg constraints (charset/pattern, leading-dash
rejection, numeric range, list caps). Violations return userError so no
handler sees malformed input. Schema adoption is opt-in per command —
unschema'd commands dispatch unchanged.

panel.resize adopts a schema (dropping the _ResizeArgs hand-lift from
T-119); git.checkout and git.push gain schemas that reject leading-dash
refs at the dispatcher and, via positional ordering, fix the C-client
CLI path — `clide git checkout <branch>` now reaches the handler, where
the positional token previously never mapped to `branch`. The T-104
validateGitRef + count/path caps stay in place as defense-in-depth
because the git client is reachable directly from the UI, not only
through the dispatcher.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-20 18:02:30 +02:00
co-authored by Claude
parent 97c83b8fc9
commit 498bbc1205
8 changed files with 504 additions and 71 deletions
+22 -2
View File
@@ -1,4 +1,5 @@
import 'package:clide/clide.dart' show clideVersion;
import 'package:clide/src/ipc/command_schema.dart';
import 'package:clide/src/ipc/envelope.dart';
import 'package:clide/src/ipc/schema_v1.dart';
@@ -12,13 +13,26 @@ class DaemonDispatcher {
final Map<String, CommandHandler> _handlers = {};
void register(String cmd, CommandHandler handler) {
/// Per-command argument schemas (T-120 / D-74). Populated alongside
/// handlers by registrants. A command with no entry dispatches
/// unvalidated — schema adoption is opt-in per command.
final Map<String, CommandSchema> _schemas = {};
/// Register [handler] for [cmd]. Pass [schema] to have the dispatcher
/// normalise + validate `req.args` before the handler runs (D-74).
void register(String cmd, CommandHandler handler, {CommandSchema? schema}) {
_handlers[cmd] = handler;
if (schema != null) {
_schemas[cmd] = schema;
} else {
_schemas.remove(cmd);
}
}
/// Remove all registered handlers except ping/version.
void clear() {
_handlers.removeWhere((k, _) => k != 'ping' && k != 'version');
_schemas.removeWhere((k, _) => k != 'ping' && k != 'version');
}
bool get isEmpty => _handlers.length <= 2; // only ping + version
@@ -36,7 +50,13 @@ class DaemonDispatcher {
),
);
}
return h(req);
final schema = _schemas[req.cmd];
if (schema == null) return h(req);
final result = schema.validate(schema.normalize(req.args));
if (!result.isOk) {
return schemaError(req.id, result.error!);
}
return h(IpcRequest(id: req.id, cmd: req.cmd, args: result.values!));
}
Future<IpcResponse> _ping(IpcRequest req) async => IpcResponse.ok(