Skip to content

feat(managed): mint agent API token with instance-ops permissions - #92

Merged
medcl merged 19 commits into
mainfrom
agent-api-token-permissions
Sep 18, 2026
Merged

medcl merged 19 commits into
mainfrom
agent-api-token-permissions

Conversation

@medcl

@medcl medcl commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

Summary

Mint the self AGENT_API_ACCESS_TOKEN with security.InstanceOpsPermissionKeys() so manager-originated calls keep passing after the framework moves its operational routes (stats, queue browsing, config list/runtime, keystore read/write, pipeline task ops, log tail) onto the web port behind RequireLogin + RequirePermission.

The permission filter denies by default, and existing deployed tokens were minted with no permissions — so getOrCreateAgentAPIToken now:

  1. reuses the stored token only while its KV record still grants every required key;
  2. otherwise re-mints with the full set, updates the keystore, and the exchange flow re-registers the fresh token with the manager.

Dependencies

medcl added 13 commits August 15, 2026 11:56
- plugin/plugin.go: aggregate agent-side plugins (logs_processor,
  elastic logging/metric, agent enterprise) plus the framework
  enterprise data-processing processors (dissect, json, date, mutate,
  field_standardize, otel_normalize, drop_fields, drop_event, geoip)
  and the otlp_export processor, so their init() registrations fire on
  boot; fixes the broken _ "infini.sh/agent/plugin" import in main.go
- agent.yml: document the opt-in logs_otlp_egress pipeline
  (consumer(logs) -> for_each(dissect, field_standardize) ->
  otlp_export -> gateway OTLP intake)
- deps: maxminddb-golang, otlp/proto (via framework plugins)
…s it

Ports internal/managed from pr/agent-managed-token-flow-20260524 (adapted
to the current CreateAPIToken signature and post-register hook shape):
at startup the agent creates an access token via the framework's token
manager (persisted in its keystore), and after registering to the config
manager exchanges it so the manager can call back into the agent
(pipeline tasks, stats — the LogPilot ingestion detail panel).
…KEN]])

Follows the established MANAGED/REMOTE_CONFIG_SERVERS pattern so the
one-time registration pass can be passed via -e ENROLLMENT_TOKEN=et-...
without editing the yaml.
Dials the config manager's /ws endpoint (derived from configs.servers,
carrying the instance ID and manager credential), handshakes via
HELLO, and serves the manager's proxied HTTP requests as loopback
calls against the agent's own web port — the manager can now reach
agents that sit behind NAT/firewalls and are only one-way reachable.
Reconnects every 5s; module priority 101 (after managed bootstrap so
the manager token is already in the keystore).
…tly dropped

The websocket hub frames every message as "<MsgType> <payload>": the
proxied requests arrive as "PRIVATE reverse_request {json}". The
client dispatched on the FIRST space-delimited token ("PRIVATE"), so
every request fell through the switch unanswered and the manager's
ProxyRequest hit its 30s deadline.

Strip the type prefix (CONFIG/PRIVATE), then dispatch on the command
(reverse_request). Matches the console agent's reference dispatch.
Requests also execute in a goroutine now, so a slow call no longer
blocks the read loop.
logs config gains queue_type: "kafka" routes tailed log envelopes into a
kafka-type bus queue instead of the local disk queue (ship_direct path
stays for direct OTLP). ship emitter tags envelopes with the source queue
type; the kafka_queue plugin is imported in plugin.go/main.go so the
backend is available purely via configuration. Includes the reverseclient
import switch and go.mod/go.sum refresh that the current tree builds on.
Importing modules/queue exposes /queue/stats on the agent API, so the
logpilot sampler can pull local staging queue depth/consumer offsets via
the reverse channel (previously 404 — agent-side backlog was invisible).
…reverseclient

The agent-side reverse websocket/HTTP channel now lives in the framework
(modules/configs/reverseclient), shared with gateway — the binary already
blank-imports it from main.go. Drop the local copy.
…tance

- harvester: detect .gz paths and read the decompressed stream from the
  start (immutable archives — offsets in it are not comparable across
  runs, so state tracking stays mtime-based)
- file detector: track files seen in the last walk; a re-created path
  carrying the same file identity (rename/move) inherits the old offset
  instead of re-reading from the beginning
- kafka_queue: local bus backend for transport=kafka streams (logs
  processor writes, gateway consumes)
- transfer.receiver_port_range: 30000-30100 for receiver listeners
- enable web.embedding_api for the aggregation UI
/elasticsearch/logs/_list and /elasticsearch/logs/_read accepted any
caller-supplied logs_path (e.g. /etc) and served whatever the agent
process could read. Reads are now confined to:

- elasticsearch_logs.allowed_paths from the agent config, the escape
  hatch for layouts discovery cannot see
- log directories the local search nodes report themselves (settings
  path.logs, path.home/logs fallback), discovered via the same process
  scan the console derives its paths from (60s TTL cache, stale cache
  survives refresh failures, no source at all means deny)

System paths are never readable (util.IsSystemReadPath), traversal and
symlink escapes are rejected by util.ReadGuard, non-regular files are
skipped (a fifo no longer hangs row counting), and denied requests get
403 with a remediation hint. Console/cloud flows keep working unchanged
since the paths they pass come from the agent's own discovery.

🤖 Generated with ZCode
…out blocking

The whitelist missed directories the console derives from the search
processes' command lines, so GC logs placed outside settings path.logs
(-Xlog:...:file=) or a -Des.path.logs override returned 403. The agent
now mirrors that derivation (path.logs override, path.home/logs, gc file
dir) on top of the node settings scan, and logs_path may be a
subdirectory of a whitelisted root via ReadGuard.ContainsUnder.

Refreshes no longer hold request handling hostage: once a guard exists
it serves even when stale while a single background refresh runs; only
the first caller builds synchronously.

🤖 Generated with ZCode
The framework's operational routes (stats, queue browsing, config
list/runtime, keystore read/write, pipeline task ops) are moving to the
web port behind RequireLogin + RequirePermission, and the permission
filter denies by default. The self-minted AGENT_API_ACCESS_TOKEN
therefore carries security.InstanceOpsPermissionKeys() so manager-
originated calls (reverse channel loopback, direct endpoint access)
keep passing.

Existing tokens were minted with no permissions; they are now re-minted
at startup when the stored record no longer covers the required keys,
and the exchange flow re-registers the fresh token with the manager.

Depends on infinilabs/framework instance-ops-routes-rbac.
@medcl
medcl marked this pull request as draft September 10, 2026 14:53
Replace the remaining Chinese comments in the PR diff (agent.yml,
main.go, plugin/logs) with English equivalents and apply the gofmt
alignment fix in plugin/logs/logs.go required by the CI format check.
- agent.yml: keep WEB_BINDING on port 23000, default MANAGED to false,
  restore the previous memory-limit comment, and remove the active
  kafka_queue section (opt-in via the documented example instead)
- plugin/plugin.go: rely on make update-plugins discovery for
  enterprise plugins instead of importing them from source, so public
  builds resolve without the private checkouts
- go.mod/go.sum: go mod tidy against the public tree (matches the
  convention on main)
@medcl
medcl marked this pull request as ready for review September 17, 2026 11:58
The CI checkout of framework main does not ship util.ReadGuard yet
(it lands with framework#420, still unmerged), which broke the
plugin/api build and with it unit_test and code_lint.

Vendor the framework's saferead (ReadGuard, NewReadGuard,
IsSystemReadPath, canonicalPath) plus its tests into lib/util and
switch plugin/api/log_whitelist.go to the local copy. Drop the
vendored files and go back to infini.sh/framework/core/util once a
framework release carrying #420 becomes a hard dependency.
- logs processor: derive scanCtx from the pipeline context instead of
  forwarding cancellation through a watcher goroutine; the embedded
  stdlib cancelCtx makes context.WithCancel link the cancellation via
  the parentCancelCtx fast path, so no extra goroutine is spawned and
  in-flight walks still abort when the pipeline stops
- es logs whitelist: the CAS-winning request refreshes the stale
  cache inline while concurrent requests keep serving the cached
  guard; removes the background refresh goroutine (and with it the
  unrecovered-panic risk on a bare goroutine)
@medcl
medcl merged commit 278374b into main Sep 18, 2026
8 of 13 checks passed
@medcl
medcl deleted the agent-api-token-permissions branch September 18, 2026 05:04
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