Commit Graph

64 Commits

Author SHA1 Message Date
ryan e0f2309520 feat(router): add whitelist mechanism for http driver and auth plugin
- implement route whitelist registration and wildcard matching in RouterExtension
- add cookie store session fallback when Redis is disabled in driver_http
- actively register public auth endpoints to whitelist in auth plugin
- update user handlers to persist session and clear cookie on logout
- document router whitelist mechanism in AGENTS.md and new-api skill
2026-08-29 11:39:13 +08:00
Ryan 53ae3007d0 fix(cordis): fail-closed auth guards for user/message_gateway/admin (#1)
* autoresearch iter 23: fail-closed auth guarding for user/message_gateway

Both plugins resolve contracts.AuthService in Apply to build their route
middleware, but declared only DBService in Inject(). The kernel gates a
plugin's Apply solely on declared deps, and cmd/app.go registers user
before auth, so user mounted first, core.Inject failed, and loginMW
silently degraded to a pass-through closure — leaving /api/v1/user
change-password, profile and access-tokens unguarded. message_gateway
was saved only by its later list position.

Declare AuthService in Inject() for both, and pin the property with a
reconcile-level test that mirrors production registration order and
asserts the real auth middleware reaches the route table.

* autoresearch iter 24: make auth middleware fallbacks fail closed

user, message_gateway and admin each fell back to a c.Next() closure when
contracts.AuthService could not be resolved, so a route would be served as
if authenticated. For admin this is reachable at runtime: OnDispose calls
service.ResetServices(), which nils the global the per-request guard reads,
so requests still in flight during dispose bypass authorization entirely.

Add ginutil.AuthUnavailable() and bind every fallback to it, with a test
that drives each plugin's registered guard without an auth service present
and asserts the request is aborted rather than passed through.

* chore(autoresearch): log iter 23 (fail-open auth ordering, proven)

* autoresearch iter 24 follow-up: let staticcheck infer the auth guard type

* docs(autoresearch): log iters 24-25 and lessons 9-11 (declared-dep bug class, gate discipline)
2026-08-29 11:21:04 +08:00
ryan b624de9620 feat(cmd): log actual plugin migration version instead of up-to-date 2026-08-29 11:11:51 +08:00
ryan 4d65e57f9a merge: feat(core): implement cordis configuration extension and migrate all plugins 2026-08-29 10:54:28 +08:00
ryan ed8491addf feat(core): implement cordis configuration extension and migrate all plugins 2026-08-29 10:53:53 +08:00
ryan 8bd59511a8 refactor(config): make legacy loader reentrant and add engine parity test
load(configPath, testMode) 用私有 viper 实例替代包级全局,使同一输入可反复
求值;对拍测试以入库的 config.example.yaml 为必备基准(本地 config.yaml
存在时加测),在四类 env 场景下逐 key 比对新引擎与旧装载器,并以变异检验
确认其能发现漂移。
2026-08-29 10:15:32 +08:00
ryan 696809899e feat(infra): add viper backed configuration source adapter
实现 core.ConfigSource:按 CONFIG_PATH 或向上查找定位 config.yaml,缺文件
降级为纯环境变量来源,坏文件返回错误而非 log.Fatalf,并把 key 命中与"设为
零值"区分开来。viper 依赖被隔离在此包,内核保持零具体运行时依赖。
2026-08-29 10:06:46 +08:00
ryan 4acb529b85 feat(core): add config resolution barrier and plugin gating to App
App 新增 WithConfigSource / WithConfigDecl / Prepare / ShutdownTimeout /
SetShutdownTimeout;Use 收集门禁插件的提前声明,调和循环内求值门禁并跳过
被关闭的插件,使组合根无需再跨插件读配置选实现。未注入配置源的 App 保持
原行为,被门禁但无配置源则 fail fast 点名原因。
2026-08-29 10:03:56 +08:00
ryan ad8384182c autoresearch iter 22: delete lint suppressions that suppress nothing
24 of the 96 nolint directives were dead: they covered findings that no
longer exist. A stale suppression is not inert — it silently claims any
future finding for that linter in that scope, so a real problem raised
there would vanish without anyone noticing. Explanatory prose was kept as
ordinary comments.

Two directives proved load-bearing under the project gate even though
nolintlint reported them unused, and removing them exposed verified
contextcheck false positives: App.Run does forward a sigCtx derived from
the caller's context to Start, and the migration lock renewal must keep
its own deadline because the task context may already be canceled. Both
were restored, narrowed to the live linter, and given the reason the
originals lacked.
2026-08-29 09:54:33 +08:00
ryan afaa8f79eb feat(core): add skipped fiber state for configuration gates
Fiber 新增 SKIPPED 态与 Skip/Skipped 方法:门禁为假的插件在 Apply 之前
即被排除并释放其作用域 Context,为互斥实现(cache 与 cache_memory 等)
同时挂载由内核择一激活铺路。
2026-08-29 09:53:10 +08:00
ryan 39a81f7723 feat(core): mount the configuration extension point on the kernel Context
Context 新增 Config() 访问器,注册表随 Fork 共享(配置声明是进程级事实),
并在 types.go 导出配置别名与 ConfigGatedPlugin 可选接口,为插件门禁做准备。
2026-08-29 09:47:59 +08:00
ryan c0869cb878 feat(core): expose read-only config view, generic getter and redacted dump
补齐 Value/String/Bool/Int/Duration/Strings/WasSet/Origin 只读访问器与
按 secret 脱敏的 Entries 导出,新增 core.ConfigGet[T] 泛型读取入口,并用
编译期断言钉住 ConfigRegistry 对 ConfigExtension 的完整实现。
2026-08-29 09:43:33 +08:00
ryan 1023fa3adb autoresearch iter 21: remove the telegram inbound media scratch dir after handling
downloadMedia created a fresh os.MkdirTemp for every private message carrying
a photo or document, and no code path anywhere reads Attachment.Path, so each
message permanently grew the disk while burning a Bot API download. The
handler now removes the directory once onInbound returns.

No mechanical proof is possible here: exercising downloadMedia needs a live
telebot download. Verified by reading every consumer of InboundMessage
.Attachments instead.
2026-08-29 09:40:49 +08:00
ryan 4653afc554 feat(core): resolve declared configuration with env and file precedence
按 显式 env > autoEnable > 配置文件 > default 的优先级链解析每个已声明
key,支持标量 env 填充切片、duration 与结构体切片解码,非法 env 值不再
静默回退而是报 ErrConfigType。
2026-08-29 09:38:39 +08:00
ryan c77b5358e2 feat(core): add configuration declaration registry
配置读取框架的内核侧抽象:插件用带 config/env/default/autoEnable/secret
tag 的结构体声明自己读哪些字段,注册表按 key 归集并对重复声明做一致性
校验,为后续按声明解析与门禁求值提供基础。
2026-08-29 09:32:53 +08:00
ryan efa75558af autoresearch iter 20: give the telegram poller a real long-poll window
telebot types LongPoller.Timeout as time.Duration and sends
int(timeout / time.Second) to getUpdates, so the literal 10 meant ten
nanoseconds: Telegram received timeout=0, long polling never held the
connection, and the adapter polled the Bot API in a tight loop instead.
Use 10 seconds and extract the settings so the conversion is asserted.

The adapter also has no media temp-dir cleanup (downloadMedia creates an
MkdirTemp per attachment and nothing removes it); that is left as a separate
change rather than bundled here.
2026-08-29 09:32:27 +08:00
ryan 84eaf3f555 autoresearch iter 19: make task handlers driver-agnostic so they run under both workers
upload's four real background tasks (system cleanup, stats rebuild, storage
migration, image warmup) plus the admin and user stubs registered handlers
typed as func(ctx, *asynq.Task) error. Only the asynq worker accepts that
shape; the Redis-free in-process worker's invokeHandler rejects it with
'unsupported handler type', so none of those tasks could ever run in that
deployment mode. Take payload bytes instead, which both drivers support.

Adds architecture gate check 7 forbidding asynq imports from business and
infrastructure plugins. It deliberately does not cover robfig/cron: the admin
plugin uses cron.ParseStandard only to validate a user-entered spec, which is
a library call rather than a driver binding, and the in-process scheduler
already normalizes 5-field specs.
2026-08-29 09:23:01 +08:00
ryan 8c4955c835 autoresearch iter 18: remove the phantom user:daily_audit schedule
The user plugin registered a cron dispatching to user:daily_audit, a task
pattern it never registers, and no audit logic exists anywhere in the plugin.
The daily run therefore went nowhere while a test asserted the schedule was
registered — proving the wiring existed, not that it worked. Implementing a
real daily audit is unstarted functionality, so the schedule is removed rather
than stubbed.

The combined domain test now asserts the real invariant across all applied
plugins: every schedule's task type must have a registered handler.
2026-08-29 09:11:16 +08:00
ryan 1b1c45206c autoresearch iter 17: wire the pairing-code cleanup cron to a real handler
message_gateway scheduled message_gateway:cleanup_pairing_codes every 10
minutes but never registered a task under that pattern, so every dispatch
went to a task type with no handler and expired pairing rows accumulated
forever, even though repository.DeleteExpiredPairingCodes already existed.
Add a test that fails for any schedule whose task pattern is unregistered:
it reports the exact orphan rather than relying on a schedule-exists assert.
2026-08-29 09:03:33 +08:00
ryan 976f9b15ae autoresearch iter 16: add batch user lookup and use it for log enrichment
enrichAccessLogsWithUsers preferred the UserService contract over the local
repository — correct layering, but it looped GetUserByID and issued up to a
page-size worth of separate SELECTs against w_users, while the single-query
WHERE id IN variant was only reached in the no-contract fallback branch.
Give the contract a GetUsersByIDs so callers can keep the layering and drop
the N+1. The test asserts 1 query batched against 3 per-id, so the counting
itself is checked.
2026-08-29 08:51:30 +08:00
ryan 2c415638fd autoresearch iter 15: stop CORS from querying the database on every request
isOriginAllowed read server_address from w_system_configs for every request
carrying an Origin header — one uncached primary-DB round-trip plus a split
and trim loop per browser request, while sibling config reads in the storage
driver are already TTL cached. Read it through the shared CacheService with
the same 5s window, falling back to the database when no cache is bound.
driver_http now binds CacheService in Apply the way it already binds DBService.
2026-08-29 08:44:37 +08:00
ryan 6932b54a30 autoresearch iter 14: stop a cache read error from clobbering the buffered task log
AppendTaskExecutionLog discarded the error from its read of the buffer, so a
transient cache failure looked like an empty buffer and the very next write
replaced the whole accumulated log with just the newest line. Flush already
distinguished miss from failure; append now does the same.
2026-08-29 08:38:55 +08:00
ryan 101cb2ff7a autoresearch iter 12: inproc driver reports untracked executions as an error
GetExecution returned (nil, nil) under the in-process driver where the asynq
driver returns an error, so the same contract call meant 'empty' in one
deployment mode and 'failed' in the other.
2026-08-29 08:35:00 +08:00
ryan 3d2038a251 autoresearch iter 11: unimplemented auth mocks fail loudly instead of returning (nil, nil)
Authenticate, CreateAuthSource, UpdateAuthSource and ToggleAuthSource claimed
success with a nil record, so any test that reached them surfaced a nil
pointer dereference instead of the actual cause. Full suite confirms no test
relied on the silent behaviour.
2026-08-29 08:33:16 +08:00
ryan c4068efd54 autoresearch iter 10: keep error identity at the last errorlint sites
RunPushTest flattened channel validation failures with %v, telegram's
fallback path discarded the original send error, and the config loader's
type assertion on viper.ConfigFileNotFoundError would miss a wrapped form
and fatally abort over a merely missing file. errorlint now reports zero.
2026-08-29 08:29:40 +08:00
ryan 22ecafdbc2 autoresearch iter 9: drop always-nil error results from task log loaders
loadTaskExecutionLog and loadTaskExecutionLogs could never fail, yet four
call sites branched on their error as if they could, presenting unreachable
code as error handling.
2026-08-29 08:26:39 +08:00
ryan 18820b17f6 autoresearch iter 8: render synthesized notification content in stable order
bodyContent's fallback ranged over the body map, and Go randomizes map
iteration, so the same notification rendered its fields in a different order
on every send. Observed failing before the fix: the second call already
reordered the output. Iterate sorted keys instead.
2026-08-29 08:23:23 +08:00
ryan c66399eedc autoresearch iter 7: share body field extraction across push channels
email, telegram and lark each re-implemented the title/content/level lookup
with only their markup differing, and each carried a dead content := ""
initialization that every branch overwrote. Three small helpers in template.go
now own that logic.
2026-08-29 08:20:43 +08:00
ryan ce33997c23 autoresearch iter 6: reject negative cursor instead of silently using 0
parsePositiveInt reported invalidity through a bool that both call sites
discarded, and returned (false, nil) whenever Atoi succeeded on a negative
number. GetLogs therefore accepted ?cursor=-5 and served it as cursor 0
('latest') instead of the documented 400. Validity now travels through the
error result, which no caller can ignore.
2026-08-29 08:16:50 +08:00
ryan 381c79417e autoresearch iter 5: recover panics in background cleanup loops
Four long-running goroutines were launched with a bare go statement, so a
panic in any of them took down the whole process: the RAM cache's expired-key
eviction, the batch writer's flush worker, the disk cache cleanup worker and
the PoW memory store sweeper. Route them through util.Go.

The architecture gate only grepped for 'go func(', which is why the named-call
form went unnoticed; widen it to cover both launch styles.
2026-08-29 08:12:27 +08:00
ryan 7e6b9e7c2f autoresearch iter 4: stop one disconnected client from failing a shared image flight
EnsureCompressedImageCache passed the arriving caller's request context into
the singleflight body, which runs once for every concurrent requester of that
cache key. If the first client disconnected, gin canceled the context, the
shared generation aborted, and every follower received that failure and fell
back to the uncompressed original. Detach cancellation with
context.WithoutCancel so trace values still propagate but the shared work
outlives any single requester.
2026-08-29 08:07:19 +08:00
ryan 686e3ef5a6 autoresearch iter 3: share upload-record error mapping via filesrv helper
ServeFileByID and DownloadFile duplicated the lookup failure mapping and
each used an unchecked *strconv.NumError assertion that cannot match a
wrapped error. One helper now classifies 404 vs 400 via errors.As; each
endpoint keeps its own fallback for unclassified failures. ErrInvalidUploadID
became unused once both sites report ErrInvalidFileID for a malformed ID.
2026-08-29 08:02:42 +08:00
ryan 37ad58699d autoresearch iter 2: compare error sentinels with errors.Is
Five sites used == against sentinels (redis.Nil, ingest.ErrForbidden,
errs.ErrDatabaseUninitialized). The neighbouring not-found checks already
went through errors.Is helpers, so a wrapped error would silently downgrade
a 403 to a 400 and a 500 to a 400.
2026-08-29 07:58:59 +08:00
ryan 1c5731bf75 autoresearch iter 1: keep Using2/Using3 dependency causes reachable
Using2/Using3 flattened per-dependency injection failures with %v while
Using1 wrapped with %w, so errors.Is could not see ErrServiceNotFound
through a multi-dependency resolution failure.
2026-08-29 07:56:02 +08:00
ryan f4975d6732 refactor(plugins): restructure admin and message_gateway into standard layered sub-packages 2026-08-28 22:33:26 +08:00
ryan 0035e548a5 fix(risk_control,message_gateway): fix SQL LIKE escape syntax and use UserService contract 2026-08-28 20:19:24 +08:00
ryan df351cbd33 refactor(core): decouple gin from pkg/util and reduce code duplication 2026-08-28 20:15:47 +08:00
ryan c52b7c4abf test(upload): move storage task fixtures to in-memory mock 2026-08-28 18:54:23 +08:00
ryan 4d6be2fa77 fix(drivers): propagate app-lifetime context through inproc cron/worker drivers
cron 触发与 worker 执行的任务现在继承应用生命周期 context(关闭时级联取消,带超时子上下文),
替代裸 context.Background()。contextcheck 清零。
lint_issues 26→24
2026-08-28 17:04:44 +08:00
ryan 867fcb2288 refactor: revive cleanup — unused params to _, add missing doc comments
- admin/db_helper GetCache/GetUserService/GetAuthService: ctx -> _ (签名对称保留)
- validateMergedStorageConfig / ParseMigrationTargetConfig / MockStorageService.Put 未用参数 -> _
- SetDBServiceForTest、StorageDriver 常量组补充文档注释
lint_issues 33→26
2026-08-28 16:53:32 +08:00
ryan 6e12a7fd5d fix(admin): propagate cache errors in FlushTaskExecutionLog
缓存故障(非 ErrCacheMiss)不再被静默吞掉:上抛包装错误,避免缓冲任务日志丢失并误报持久化成功;
ErrCacheMiss 仍视为无日志的正常路径。附 3 个回归测试(故障/未命中/持久化+清理)。
lint_issues 34→33 (nilerr 清零)
2026-08-28 16:50:03 +08:00
ryan cf85a56aa7 refactor: eliminate string/magic-number literals (goconst, mnd) and fix const-type grouping (SA9004)
- upload/task: taskCategoryUpload/taskQueueDefault 常量替代 8 处字面量
- admin: 复用既有 logDBNameSQLite 常量替代 3 处 "sqlite" 字面量
- pkg/cache/disk: defaultCleanupInterval 命名常量
- driver_asynq_worker/executor: 分离 contextKey 类型常量组
lint_issues 45→34, tests 44/44
2026-08-28 16:38:18 +08:00
ryan 528240026d chore(lint): unify formatting on golangci-lint fmt (gofumpt), uncap issue reporting, fix gofumpt drift
- make format 现在与 code-check 使用同一格式化器(golangci-lint fmt),消除 goimports -local 与 gofumpt 的格式拉锯
- .golangci.yml 关闭默认 50/3 截断,完整上报所有问题(只增强不弱化)
- 全库 gofumpt 规范化(203 files, 纯格式无行为变更)
2026-08-28 16:31:53 +08:00
ryan 3a61c38dc5 fix(core): delegate driver and task extension lookups to root context and improve test reliability 2026-08-28 16:09:18 +08:00
ryan 7fca53823a feat(cmd): restore startup banner display in CLI entrypoints 2026-08-28 15:46:58 +08:00
ryan 3df621637f fix(db): ensure sqlite data directory exists before opening database 2026-08-28 15:42:24 +08:00
ryan 692b4b4851 feat(db): split migrations into dialect-specific sqlite and postgres packages 2026-08-28 15:36:03 +08:00
ryan 06508b6f13 refactor(drivers): decouple inproc cron from inproc worker and satisfy linter 2026-08-28 15:17:11 +08:00
ryan 0ff117da47 feat(cmd): support dynamic zero-redis pluggable assembly in app bootstrap 2026-08-28 15:15:26 +08:00
ryan e434e6dc0a feat(drivers): provide TaskService implementation in inproc worker 2026-08-28 15:14:27 +08:00