Commit Graph

416 Commits

Author SHA1 Message Date
ryan d043e7366f docs: update developer guide and white paper with router whitelist and session fallback 2026-08-29 11:40:50 +08:00
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 b43c429544 chore(arch): forbid viper and mapstructure inside the micro-kernel
配置装载实现必须留在 plugins/infra/config 适配器里,内核只依赖
ConfigSource 抽象;把 viper 与 mapstructure 加入 1.1 禁止清单,防止配置
装载依赖重新渗回 core(已用临时探针文件反向验证检查生效)。
2026-08-29 10:17:57 +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 b3c4d6cb99 docs(autoresearch): lessons 6-8 (audit verification, counting doubles, gate+veto discipline) 2026-08-29 09:56:24 +08:00
ryan 6011effade chore(autoresearch): log iter 22 (debt 79 -> 54) 2026-08-29 09:55:24 +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 b6f2221280 chore(autoresearch): log iter 21 2026-08-29 09:41:30 +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 9c0f31fad0 chore(autoresearch): log iter 20
Note: iter 20's commit also captured an in-flight edit to
docs/superpowers/plans/2026-08-29-cordis-config-extension.md belonging to a
concurrent session, because it used 'git add -A'. Content is intact; later
iterations stage explicit paths only.
2026-08-29 09:35:50 +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 ae8bbd98f8 chore(autoresearch): log iter 19 2026-08-29 09:24:33 +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 fd83496a05 docs(config): add implementation plan for the Cordis config extension point
覆盖 P1+P2:core 配置引擎、viper 适配器隔离、门禁与 FiberSkipped、
新旧解析对拍。迁移 27 个消费文件与旧单例退场由后续计划承接。
2026-08-29 09:20:22 +08:00
ryan 020ebebfaa chore(autoresearch): log iter 18 2026-08-29 09:12:03 +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 d80f9d209b chore(autoresearch): log iter 17 2026-08-29 09:03:54 +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 84946977bf chore(autoresearch): log iter 16 (perf, contract batch) 2026-08-29 08:52:18 +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 5df282f296 chore(autoresearch): log iter 15 (perf, proven) 2026-08-29 08:45:21 +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 2654eb6e2c chore(autoresearch): correct iter 14 log (debt held at 79, kept via proven-fix gate) 2026-08-29 08:39:53 +08:00
ryan 45bf1d8933 chore(autoresearch): log iter 14 2026-08-29 08:39:38 +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 00ab727791 chore(autoresearch): log iter 12 (debt 80 -> 79) 2026-08-29 08:35:26 +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 2c8020188d chore(autoresearch): log iter 11 (debt 84 -> 80) 2026-08-29 08:33:56 +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 643bfca996 chore(autoresearch): log iter 10 (debt 87 -> 84, errorlint 12 -> 0) 2026-08-29 08:30:33 +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 f7fd980429 chore(autoresearch): log iter 9 (debt 89 -> 87) 2026-08-29 08:27:21 +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 a529700ed3 chore(autoresearch): log iter 8 (proven bug fix, debt held at 89) 2026-08-29 08:24:09 +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 03f48a9a80 chore(autoresearch): log iter 7 (debt 92 -> 89) 2026-08-29 08:21:24 +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 bf364f4036 chore(autoresearch): log iter 6, distinguish compile-level from assertion-level proof 2026-08-29 08:17:59 +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