Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fd3f16b
feat(configs): managed-config protocol server with token authentication
medcl Aug 18, 2026
8f1834d
feat(queue): queue_output processor — chain-tail batch→queue bridge
medcl Aug 19, 2026
6568c19
feat(pipeline): expose LookupProcessorConstructor
medcl Aug 19, 2026
c0b46ea
feat(pipeline): clone convention — CloneContextKey + host materializa…
medcl Aug 19, 2026
0f23f79
fix(configs): upsert treated first-registration not-found as a 500
medcl Aug 19, 2026
c676550
feat(model): instance AccessToken + Token type (console exchange conv…
medcl Aug 19, 2026
c8eeff7
feat(configs): wire the framework token manager into the managed channel
medcl Aug 19, 2026
8ec9dc6
fix(configs): sync 401 — client sends the self token, server only che…
medcl Aug 19, 2026
c08a3a6
feat(configs): instance admission — register as pending, admin approves
medcl Aug 19, 2026
4427e6b
fix(configs): pending instances no longer 401 on sync; bootstrap opti…
medcl Aug 19, 2026
3f8396a
feat(configs): enrollment tokens — one-time registration tickets + re…
medcl Aug 19, 2026
ffee24b
feat(app): -e KEY=VALUE flag — environment overrides from the command…
medcl Aug 19, 2026
d8ea398
fix(configs): re-register also rejected the agent's self token
medcl Aug 19, 2026
073fc2b
refactor(configs): manager credentials now come from the framework to…
medcl Aug 19, 2026
662d5ee
fix(configs): heartbeat clobbered approved instances back to pending
medcl Aug 20, 2026
df63b81
fix(configs): register read approval from the payload, not the store
medcl Aug 20, 2026
574ce7b
fix(configs): exchange rejected the credentials it exists to serve
medcl Aug 20, 2026
96f09a1
fix(model): advertised endpoint pointed at the non-serving API default
medcl Aug 20, 2026
5ee3f94
feat(configs): host the reverse channel — manager reaches one-way agents
medcl Aug 20, 2026
bcef902
fix(configs): enrollment ticket demanded on every restart
medcl Aug 20, 2026
1f697f4
refactor(configs): rename AgentAccessTokenKeystoreKey to InstanceAcce…
medcl Aug 24, 2026
488d169
fix(configs+processors): harden merge blockers found in live LogPilot…
medcl Aug 24, 2026
6907126
fix(configs): pending instance sync wiped local managed configs
medcl Aug 25, 2026
bde0c6a
fix(configs): reverse client loopback rewrite broke specific-IP bindings
medcl Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"os/signal"
"runtime"
"runtime/debug"
"strings"
"sync"
"syscall"
"time"
Expand Down Expand Up @@ -184,6 +185,14 @@ func (app *App) initWithFlags() {
flag.StringVar(&app.svcFlag, "service", "", "service management, options: install,uninstall,start,stop")
flag.StringVar(&app.svcUser, "service-user", "", "OS user account used to run the service")

// -e KEY=VALUE (repeatable): environment overrides applied BEFORE the
// config loads. Picked up by $[[env.KEY]] template expansion in the
// YAML (OS env wins over the YAML env: section), so deployments can
// parameterize any templated setting from the command line instead of
// editing config files. Example:
// agent -e MANAGED=true -e REMOTE_CONFIG_SERVERS=http://lp:29000
flag.Var(&cliEnvOverrides{}, "e", "environment override KEY=VALUE (repeatable, feeds $[[env.KEY]])")

if debugFlagInitFunc != nil {
debugFlagInitFunc()
}
Expand Down Expand Up @@ -622,3 +631,20 @@ func (app *App) Run() {
log.Error(err)
}
}

// cliEnvOverrides collects repeatable -e KEY=VALUE flags; Apply sets them
// into the process environment before configuration loading begins.
type cliEnvOverrides struct {
pairs []string
}

func (c *cliEnvOverrides) String() string { return strings.Join(c.pairs, " ") }

func (c *cliEnvOverrides) Set(v string) error {
idx := strings.Index(v, "=")
if idx <= 0 {
return fmt.Errorf("-e expects KEY=VALUE, got %q", v)
}
c.pairs = append(c.pairs, v)
return os.Setenv(v[:idx], v[idx+1:])
}
9 changes: 8 additions & 1 deletion core/api/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func StartWeb(cfg config.WebAppConfig) {

registerMCPAutoUIHandler(cfg)

mountedFuncPatterns := map[string]bool{} // embedding_api 已挂载的 ServeMux 模式
if cfg.EmbeddingAPI {
if registeredAPIMethodHandler != nil {
for k, v := range registeredAPIMethodHandler {
Expand All @@ -125,13 +126,19 @@ func StartWeb(cfg config.WebAppConfig) {
for k, v := range registeredAPIFuncHandler {
log.Debug("register http handler: ", k)
uiServeMux.HandleFunc(k, v)
mountedFuncPatterns[k] = true
}
}
}

if cfg.WebsocketConfig.Enabled {
websocket.InitWebSocket(cfg.WebsocketConfig)
uiServeMux.HandleFunc("/ws", websocket.ServeWs)
// embedding_api 可能已把 API 域注册的 /ws (StartAPI 的
// HandleAPIFunc) 挂到本 mux — 重复 HandleFunc 在 Go 1.22+
// 的 ServeMux 语义下 panic, 跳过。
if !mountedFuncPatterns["/ws"] && !mountedFuncPatterns[cfg.WebsocketConfig.BasePath] {
uiServeMux.HandleFunc("/ws", websocket.ServeWs)
}
if registeredWebSocketCommandHandler != nil {
for k, v := range registeredWebSocketCommandHandler {
log.Debug("register websocket handler: ", k, " ", v)
Expand Down
11 changes: 5 additions & 6 deletions core/api/websocket/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ package websocket
import (
log "github.com/cihub/seelog"
"github.com/gorilla/websocket"
"infini.sh/framework/core/global"
"infini.sh/framework/core/util"
"net/http"
"strings"
Expand All @@ -48,8 +47,10 @@ const (
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10

// Maximum message size allowed from peer.
maxMessageSize = 512
// Maximum message size allowed from peer. The reverse channel streams
// chunked proxied HTTP bodies through this hub (base64 chunks of up to
// 32KB), so this must comfortably exceed a chunk frame.
maxMessageSize = 8 * 1024 * 1024
)

var upgrader = websocket.Upgrader{
Expand Down Expand Up @@ -212,9 +213,7 @@ func ServeWs(w http.ResponseWriter, r *http.Request) {
for _, v := range callbacksOnConnect {
err := v(c.id, w, r)
if err != nil {
if global.Env().IsDebug {
log.Error(err)
}
log.Warnf("websocket connection rejected: %v", err)
closeMessage := websocket.FormatCloseMessage(websocket.ClosePolicyViolation, err.Error())
if closeErr := ws.WriteMessage(websocket.CloseMessage, closeMessage); closeErr != nil {
log.Error("Failed to send close message:", closeErr)
Expand Down
12 changes: 7 additions & 5 deletions core/config/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,14 @@ type ConfigsConfig struct {
ValidConfigsExtensions []string `config:"valid_config_extensions"`
TLSConfig TLSConfig `config:"tls"` //server or client's certs
ManagerConfig struct {
LocalConfigsRepoPath string `config:"local_configs_repo_path"`
BasicAuth BasicAuth `config:"basic_auth"`
LocalConfigsRepoPath string `config:"local_configs_repo_path"`
BasicAuth BasicAuth `config:"basic_auth"`
AccessToken ucfg.SecretString `config:"access_token"`
} `config:"manager"`
AlwaysRegisterAfterRestart bool `config:"always_register_after_restart"`
AllowGeneratedMetricsTasks bool `config:"allow_generated_metrics_tasks"`
IgnoredPath []string `config:"ignored_path"`
EnrollmentToken ucfg.SecretString `config:"enrollment_token"` // one-time registration pass (configs.server.enrollment.required)
AlwaysRegisterAfterRestart bool `config:"always_register_after_restart"`
AllowGeneratedMetricsTasks bool `config:"allow_generated_metrics_tasks"`
IgnoredPath []string `config:"ignored_path"`
}

type BasicAuth struct {
Expand Down
29 changes: 29 additions & 0 deletions core/model/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Framework is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello@infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package model

const (
CredentialIDSystemKey = "credential_id"
API_TOKEN = "X-API-TOKEN"
)
26 changes: 25 additions & 1 deletion core/model/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,24 @@ type Instance struct {
Labels map[string]string `json:"labels,omitempty" elastic_mapping:"labels:{type:object}"`
Tags []string `json:"tags,omitempty"`

// Groups is the SERVER-OWNED grouping of instances (e.g. "es",
// "gateway-edge"): managed from the management UI, used to target
// config delivery (ManagedConfig.Groups). Instances do not report it —
// registration/heartbeat upserts preserve the stored value.
Groups []string `json:"groups,omitempty" elastic_mapping:"groups:{type:keyword}}"`

//user can pass
Description string `json:"description,omitempty" config:"description" elastic_mapping:"description:{type:keyword}"`

Endpoint string `json:"endpoint,omitempty" elastic_mapping:"endpoint: { type: keyword }"` //API endpoint

Host *HostInfo `json:"host,omitempty" elastic_mapping:"host: { type: object }"`

// AccessToken is the agent's self-generated API token (the console
// token-exchange convention): managers store it at registration and
// use it for reverse calls (stats, pipeline tasks, proxying).
AccessToken *Token `config:"access_token" json:"access_token,omitempty" elastic_mapping:"access_token:{type:object}"`

Network NetworkInfo `json:"network,omitempty" elastic_mapping:"network: { type: object }"`
Services []ServiceInfo `json:"services,omitempty" elastic_mapping:"services: { type: object }"`
Status string `json:"status,omitempty" elastic_mapping:"status: { type: keyword, copy_to:search_text }"`
Expand Down Expand Up @@ -137,7 +148,20 @@ func GetInstanceInfo() Instance {

_, publicIP, _, _ := util.GetPublishNetworkDeviceInfo(global.Env().SystemConfig.NodeConfig.MajorIpPattern)

instance.Endpoint = global.Env().SystemConfig.APIConfig.GetEndpoint()
// The advertised endpoint must point at a server that actually serves
// requests. Deployments commonly disable the dedicated API port
// (api.enabled: false) and serve everything on the web port — in that
// case advertise the web address, not the (unserving) API default.
sysCfg := global.Env().SystemConfig
if sysCfg.APIConfig.Enabled {
instance.Endpoint = sysCfg.APIConfig.GetEndpoint()
} else {
schema := "http"
if sysCfg.WebAppConfig.TLSConfig.TLSEnabled {
schema = "https"
}
instance.Endpoint = fmt.Sprintf("%s://%s", schema, sysCfg.WebAppConfig.NetworkConfig.GetPublishAddr())
}

ips := util.GetLocalIPs()
if len(ips) > 0 {
Expand Down
9 changes: 9 additions & 0 deletions core/model/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* Copyright © INFINI Ltd. All rights reserved. */

package model

// Token is a bearer credential carried on registered instances (the
// agent's self-generated API token, stored at registration).
type Token struct {
Value string `json:"value,omitempty" config:"value"`
}
30 changes: 30 additions & 0 deletions core/pipeline/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,33 @@ func AppendFailureTag(ctx *Context, tag string) {
*tags = append(*tags, tag)
}
}

// CloneContextKey is the Context key under which the clone processor
// deposits cloned records; the batch-splitting host materializes them as
// additional batch members after the sub-chain finishes the original.
const CloneContextKey = "record_clones"

// AppendClone registers a clone of the current record for the host to
// materialize (see clone processor). No-op outside a record scope.
func AppendClone(ctx *Context, rec *event.Event) {
if ctx == nil || rec == nil {
return
}
if list, ok := ctx.Get(CloneContextKey).(*[]*event.Event); ok {
*list = append(*list, rec)
}
}

// TakeClones returns and clears the pending clones registered in this
// context (host-side; call after the per-record sub-chain).
func TakeClones(ctx *Context) []*event.Event {
if ctx == nil {
return nil
}
if list, ok := ctx.Get(CloneContextKey).(*[]*event.Event); ok && len(*list) > 0 {
out := *list
*list = nil
return out
}
return nil
}
10 changes: 10 additions & 0 deletions core/pipeline/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ type Constructor func(config *config.Config) (ProcessorBase, error)

var registry = NewNamespace()

// LookupProcessorConstructor resolves a processor constructor by its
// registered (bare) name — nil when unknown. Hosts use this to build
// ad-hoc chains (e.g. dry-run replay of a spec's processor list).
func LookupProcessorConstructor(name string) ProcessorConstructor {
if ctor, ok := registry.ProcessorConstructors()[name]; ok {
return ctor
}
return nil
}

func RegisterProcessorPlugin(name string, constructor ProcessorConstructor) {
err := registry.RegisterProcessor(name, constructor)
if err != nil {
Expand Down
Loading
Loading