diff --git a/.gitignore b/.gitignore index 78c2462..1c49fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,11 @@ agent-ops/rules/private/ /temp_verif/ /bin/control-plane /bin/edge +/bin/artifacts/ /bin/iop-control-plane /bin/iop-edge /bin/iop-node +/bin/iop-node-* /bin/iop-worker /bin/node /bin/worker diff --git a/Makefile b/Makefile index d1f9788..07c2055 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ -.PHONY: all build build-edge build-node dist dist-edge dist-node tidy test test-e2e test-openai-ollama proto proto-dart portal-test portal-build-web clean +.PHONY: all build build-edge build-node build-node-target pack-node-target tidy test test-e2e test-openai-ollama proto proto-dart portal-test portal-build-web clean GOFLAGS ?= -trimpath BUILD_DIR ?= bin -DIST_APPS ?= edge node +NODE_TARGET ?= $(shell go env GOOS)-$(shell go env GOARCH) +NODE_TARGET_PARTS = $(subst -, ,$(NODE_TARGET)) +NODE_GOOS = $(word 1,$(NODE_TARGET_PARTS)) +NODE_GOARCH = $(word 2,$(NODE_TARGET_PARTS)) all: build @@ -16,14 +19,12 @@ build-edge: build-node: go build $(GOFLAGS) -o $(BUILD_DIR)/iop-node ./apps/node/cmd/node -dist: - APPS="$(DIST_APPS)" GOFLAGS="$(GOFLAGS)" bin/build/field-binaries.sh +build-node-target: + @test -n "$(NODE_GOOS)" && test -n "$(NODE_GOARCH)" || (echo "NODE_TARGET must be -" >&2; exit 2) + GOOS=$(NODE_GOOS) GOARCH=$(NODE_GOARCH) go build $(GOFLAGS) -o $(BUILD_DIR)/iop-node-$(NODE_TARGET) ./apps/node/cmd/node -dist-edge: - $(MAKE) dist DIST_APPS=edge - -dist-node: - $(MAKE) dist DIST_APPS=node +pack-node-target: build-edge build-node-target + cd $(BUILD_DIR) && ./iop-edge bootstrap pack --target $(NODE_TARGET) --node-binary ./iop-node-$(NODE_TARGET) tidy: go mod tidy @@ -87,6 +88,8 @@ portal-build-web: clean: rm -f bin/iop-edge bin/iop-node bin/iop-control-plane bin/iop-worker + rm -f bin/iop-node-* rm -f bin/node bin/edge bin/control-plane bin/worker + rm -rf bin/artifacts rm -rf dist rm -f iop.db diff --git a/agent-ops/rules/project/domain/testing/rules.md b/agent-ops/rules/project/domain/testing/rules.md index b5d78dd..aa32a8c 100644 --- a/agent-ops/rules/project/domain/testing/rules.md +++ b/agent-ops/rules/project/domain/testing/rules.md @@ -15,7 +15,7 @@ last_rule_updated_at: 2026-05-28 - `Makefile` — 공식 test target, proto generation, 보조 smoke target을 선언하는 위치이다. - `bin/edge.sh` — repo 내부 edge console/server 개발 진단 helper이다. - `bin/node.sh` — repo 내부 node 연결 개발 진단 helper이다. field 사용자 기본 경로로 안내하지 않는다. -- `bin/build/field-binaries.sh` — field 배포용 edge/node 바이너리 build entrypoint이다. +- `iop-edge bootstrap pack` — Node host bootstrap용 artifact 폴더를 준비하는 공식 command이다. - `scripts/e2e-smoke.sh` — mock/real profile 기반 보조 edge-node smoke 검증이다. - `scripts/e2e-openai-ollama.sh` — OpenAI-compatible Ollama 입력 표면 보조 smoke 검증이다. @@ -53,7 +53,7 @@ last_rule_updated_at: 2026-05-28 - field Node bootstrap의 사용자 명령은 `curl -fsSL <완성된-bootstrap-url> | bash -s ` 형태를 기준으로 한다. 다른 bootstrap/enrollment 작업에서도 같은 수준의 단일 token UX를 우선 적용하고, 예외가 필요하면 사용자에게 먼저 확인한다. - repo 내부 edge-node 진단의 상세 수행 절차와 기능별 체크리스트는 `agent-ops/skills/project/e2e-smoke/SKILL.md`를 따른다. - `make test-e2e`, `scripts/e2e-smoke.sh`, `scripts/e2e-openai-ollama.sh`는 보조 smoke 명령이다. 실행할 수 있으면 보조 확인으로 기록하되, full-cycle 실제 구동이나 field bootstrap 검증을 대체하지 않는다. -- `bin/build/field-binaries.sh` 변경 시 최소 현재 host target build를 실행하고 산출물 위치, checksum 생성, node bootstrap script가 positional token UX를 유지하는지 확인한다. +- `iop-edge bootstrap pack` 또는 내장 artifact server 변경 시 최소 현재 host target build를 실행하고 artifact 폴더 위치, checksum 생성, node bootstrap script가 positional token UX를 유지하는지 확인한다. - 풀테스트에서는 실제 외부 CLI profile 검증을 필수로 수행한다. 환경, 계정, provider, 원격 endpoint 문제로 호출할 수 없거나 실패한 profile은 누락하지 말고 profile별 실패 또는 blocker로 보고한다. - 작업 최종 보고에는 실행한 테스트 명령, repo 내부 edge-node 진단 수행 여부, 보조 E2E smoke 수행 여부, full-cycle 실제 구동 수행 여부를 명시한다. 수행하지 못한 필수 검증은 이유와 남은 위험을 함께 적는다. diff --git a/agent-ops/rules/project/rules.md b/agent-ops/rules/project/rules.md index b76ae1c..b7c2adc 100644 --- a/agent-ops/rules/project/rules.md +++ b/agent-ops/rules/project/rules.md @@ -23,7 +23,7 @@ - `proto/iop/` — IOP 메시지 계약 원본이다. - `proto/gen/iop/` — protobuf 생성물이다. - `configs/` — 앱별 YAML 설정 예시이다. -- `bin/` — 개발 보조 shell entrypoint와 field binary build entrypoint이다. Edge/Node 운영 UX의 공식 표면은 `iop-edge`/`iop-node` 바이너리 command로 모은다. +- `bin/` — 개발 보조 shell entrypoint와 로컬 바이너리 산출 위치이다. Edge/Node 운영 UX와 Node bootstrap artifact 준비는 `iop-edge`/`iop-node` 바이너리 command 표면에 모은다. - `scripts/` — 보조 E2E smoke와 입력 표면 검증 스크립트이다. - `Makefile` — 빌드와 테스트 진입점을 정의한다. - `docs/` — 사람용 최신 가이드만 둔다. 환경별 값은 `agent-ops/rules/private/`에 두고, `rules.md`는 라우터로만 쓴다. diff --git a/apps/edge/README.md b/apps/edge/README.md index 075aa9c..4dc41fd 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -94,6 +94,7 @@ iop-edge env iop-edge config init iop-edge config print --config /etc/iop/edge.yaml iop-edge config check --config /etc/iop/edge.yaml +iop-edge bootstrap pack iop-edge node register iop-edge nodes list iop-edge smoke openai --model diff --git a/apps/edge/cmd/edge/main.go b/apps/edge/cmd/edge/main.go index 430c258..ab467cf 100644 --- a/apps/edge/cmd/edge/main.go +++ b/apps/edge/cmd/edge/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "crypto/sha256" "encoding/json" "errors" "fmt" @@ -13,6 +14,7 @@ import ( "path/filepath" "runtime" "sort" + "strconv" "strings" "time" @@ -61,7 +63,7 @@ The official local development and field test flow is as follows: iop-edge smoke openai --model gemma4:26b`, } root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (defaults to bundle-local edge.yaml)") - root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd(), nodeCmd(), nodesCmd(), smokeCmd()) + root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd(), bootstrapCmd(), nodeCmd(), nodesCmd(), smokeCmd()) return root } @@ -109,6 +111,16 @@ func resolveEdgeLogPath(explicit string) (string, error) { return filepath.Join(filepath.Dir(exe), "logs", "edge.log"), nil } +func resolveBootstrapArtifactDir(dir string) string { + if dir == "" { + dir = "artifacts" + } + if filepath.IsAbs(dir) { + return dir + } + return filepath.Join(binaryDir(), dir) +} + // binaryDir returns the directory of the running executable. Falls back to the // current working directory if os.Executable fails. func binaryDir() string { @@ -140,6 +152,7 @@ func serveCmd() *cobra.Command { } } cfg.Logging.Path = logPath + cfg.Bootstrap.ArtifactDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir) fx.New(bootstrap.Module(cfg)).Run() return nil }, @@ -165,6 +178,7 @@ func consoleCmd() *cobra.Command { } } cfg.Logging.Path = logPath + cfg.Bootstrap.ArtifactDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir) return runConsole(context.Background(), cfg, os.Stdin, os.Stdout) }, } @@ -309,6 +323,7 @@ func envCmd() *cobra.Command { if err != nil { return err } + cfg.Bootstrap.ArtifactDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir) renderEnv(cmd.OutOrStdout(), envReport{ ConfigPath: path, BinaryDir: binaryDir(), @@ -373,8 +388,12 @@ func addressFor(host, listen string) string { return net.JoinHostPort(host, port) } -func defaultArtifactBaseURL(host string) string { - return "http://" + addressFor(host, "0.0.0.0:18080") +func defaultArtifactBaseURL(host string, listen ...string) string { + bootstrapListen := "0.0.0.0:18080" + if len(listen) > 0 && listen[0] != "" { + bootstrapListen = listen[0] + } + return "http://" + addressFor(host, bootstrapListen) } func bootstrapScriptURL(baseURL, target string) string { @@ -397,7 +416,7 @@ func renderEnv(w io.Writer, r envReport) { artifactURL := strings.TrimSuffix(r.Bootstrap.ArtifactBaseURL, "/") artifactSource := "configured" if artifactURL == "" { - artifactURL = defaultArtifactBaseURL(r.Advertise.Host) + artifactURL = defaultArtifactBaseURL(r.Advertise.Host, r.Bootstrap.Listen) artifactSource = "derived" } @@ -406,6 +425,8 @@ func renderEnv(w io.Writer, r envReport) { fmt.Fprintf(w, "log_path: %s\n", r.LogPath) fmt.Fprintf(w, "advertise_host: %s (%s)\n", r.Advertise.Host, r.Advertise.Source) fmt.Fprintf(w, "node_transport: %s\n", addressFor(r.Advertise.Host, r.Server.Listen)) + fmt.Fprintf(w, "artifact_dir: %s\n", r.Bootstrap.ArtifactDir) + fmt.Fprintf(w, "artifact_listen: %s\n", r.Bootstrap.Listen) fmt.Fprintf(w, "artifact_base_url: %s (%s)\n", artifactURL, artifactSource) fmt.Fprintf(w, "node_bootstrap: %s\n", bootstrapScriptURL(artifactURL, runtime.GOOS+"-"+runtime.GOARCH)) if r.OpenAI.Enabled { @@ -472,6 +493,255 @@ func setupCmd() *cobra.Command { return c } +func bootstrapCmd() *cobra.Command { + c := &cobra.Command{ + Use: "bootstrap", + Short: "Prepare node bootstrap artifacts", + Long: `Prepare node bootstrap artifacts served by iop-edge serve. + +The default output directory is the configured bootstrap.artifact_dir resolved +beside the iop-edge binary. The directory is static and can be copied as-is.`, + } + c.AddCommand(bootstrapPackCmd()) + return c +} + +func bootstrapPackCmd() *cobra.Command { + var nodeBinary string + var outputDir string + var target string + var artifactBaseURL string + var edgeAddr string + c := &cobra.Command{ + Use: "pack", + Short: "Pack iop-node into the configured bootstrap artifact directory", + RunE: func(cmd *cobra.Command, _ []string) error { + cfg, err := loadEdgeConfigForBootstrapPack(cmd) + if err != nil { + return err + } + adv := resolveAdvertiseHost(cfg.Server.AdvertiseHost) + if nodeBinary == "" { + nodeBinary = filepath.Join(binaryDir(), "iop-node") + } + if outputDir == "" { + outputDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir) + } + if target == "" { + target = runtime.GOOS + "-" + runtime.GOARCH + } + if artifactBaseURL == "" { + if cfg.Bootstrap.ArtifactBaseURL != "" { + artifactBaseURL = strings.TrimSuffix(cfg.Bootstrap.ArtifactBaseURL, "/") + } else { + artifactBaseURL = defaultArtifactBaseURL(adv.Host, cfg.Bootstrap.Listen) + } + } + if edgeAddr == "" { + edgeAddr = addressFor(adv.Host, cfg.Server.Listen) + } + if err := packNodeBootstrap(nodeBootstrapPackOptions{ + NodeBinary: nodeBinary, + OutputDir: outputDir, + Target: target, + Version: version.Version, + ArtifactBaseURL: artifactBaseURL, + EdgeAddr: edgeAddr, + }); err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "wrote bootstrap artifacts to %s\n", outputDir) + fmt.Fprintf(cmd.OutOrStdout(), "bootstrap: %s\n", bootstrapScriptURL(artifactBaseURL, target)) + return nil + }, + } + c.Flags().StringVar(&nodeBinary, "node-binary", "", "iop-node binary to pack (defaults to iop-node beside iop-edge)") + c.Flags().StringVarP(&outputDir, "output", "o", "", "artifact output directory (defaults to bootstrap.artifact_dir)") + c.Flags().StringVar(&target, "target", "", "target platform label (defaults to current GOOS-GOARCH)") + c.Flags().StringVar(&artifactBaseURL, "artifact-base-url", "", "artifact base URL baked into the bootstrap script") + c.Flags().StringVar(&edgeAddr, "edge-addr", "", "edge transport address baked into node.yaml") + return c +} + +func loadEdgeConfigForBootstrapPack(cmd *cobra.Command) (*config.EdgeConfig, error) { + cfg, _, err := loadEdgeConfig(cmd) + if err == nil { + return cfg, nil + } + if !strings.Contains(err.Error(), "no edge.yaml found") { + return nil, err + } + return &config.EdgeConfig{ + Server: config.EdgeServerConf{ + Listen: "0.0.0.0:9090", + }, + Bootstrap: config.EdgeBootstrapConf{ + Listen: "0.0.0.0:18080", + ArtifactDir: "artifacts", + }, + }, nil +} + +type nodeBootstrapPackOptions struct { + NodeBinary string + OutputDir string + Target string + Version string + ArtifactBaseURL string + EdgeAddr string +} + +func packNodeBootstrap(opts nodeBootstrapPackOptions) error { + if opts.NodeBinary == "" { + return errors.New("node binary path is required") + } + if opts.OutputDir == "" { + return errors.New("output directory is required") + } + if opts.Target == "" { + return errors.New("target is required") + } + if strings.ContainsAny(opts.Target, `/\`) { + return fmt.Errorf("target %q must not contain path separators", opts.Target) + } + targetDir := filepath.Join(opts.OutputDir, opts.Target) + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return fmt.Errorf("mkdir target dir: %w", err) + } + dstNode := filepath.Join(targetDir, "iop-node") + if err := copyFile(opts.NodeBinary, dstNode, 0o755); err != nil { + return fmt.Errorf("copy iop-node: %w", err) + } + sum, err := fileSHA256(dstNode) + if err != nil { + return err + } + sumLine := fmt.Sprintf("%x iop-node\n", sum) + if err := os.WriteFile(filepath.Join(targetDir, "iop-node.sha256"), []byte(sumLine), 0o644); err != nil { + return fmt.Errorf("write iop-node.sha256: %w", err) + } + if err := os.WriteFile(filepath.Join(targetDir, "SHA256SUMS"), []byte(sumLine), 0o644); err != nil { + return fmt.Errorf("write SHA256SUMS: %w", err) + } + bootstrapDir := filepath.Join(opts.OutputDir, "bootstrap") + if err := os.MkdirAll(bootstrapDir, 0o755); err != nil { + return fmt.Errorf("mkdir bootstrap dir: %w", err) + } + script := nodeBootstrapScript(opts) + scriptPath := filepath.Join(bootstrapDir, "node-"+opts.Target+".sh") + if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil { + return fmt.Errorf("write bootstrap script: %w", err) + } + return nil +} + +func copyFile(src, dst string, mode os.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) + if err != nil { + return err + } + if _, err := io.Copy(out, in); err != nil { + _ = out.Close() + return err + } + if err := out.Close(); err != nil { + return err + } + return os.Chmod(dst, mode) +} + +func fileSHA256(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open checksum file: %w", err) + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return nil, fmt.Errorf("hash file: %w", err) + } + return h.Sum(nil), nil +} + +func shellQuote(s string) string { + return strconv.Quote(s) +} + +func nodeBootstrapScript(opts nodeBootstrapPackOptions) string { + return `#!/usr/bin/env bash +set -euo pipefail + +VERSION=` + shellQuote(opts.Version) + ` +TARGET=` + shellQuote(opts.Target) + ` +DEFAULT_ARTIFACT_BASE_URL=` + shellQuote(strings.TrimSuffix(opts.ArtifactBaseURL, "/")) + ` +DEFAULT_EDGE_ADDR=` + shellQuote(opts.EdgeAddr) + ` +POSITIONAL_NODE_TOKEN="${1:-}" +ARTIFACT_BASE_URL="$DEFAULT_ARTIFACT_BASE_URL" +EDGE_ADDR="$DEFAULT_EDGE_ADDR" +NODE_TOKEN="$POSITIONAL_NODE_TOKEN" +INSTALL_DIR="${IOP_HOME:-$HOME/iop-field}" +CONFIG_FILE="${IOP_NODE_CONFIG:-$INSTALL_DIR/node.yaml}" +BIN_FILE="$INSTALL_DIR/iop-node" +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/iop-node-bootstrap.XXXXXX")" + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +ARTIFACT_BASE_URL="${ARTIFACT_BASE_URL%/}" + +if [[ -z "$NODE_TOKEN" ]]; then + echo "[iop-bootstrap] missing node token" >&2 + echo "[iop-bootstrap] usage: curl -fsSL /bootstrap/node-$TARGET.sh | bash -s TOKEN" >&2 + exit 2 +fi + +echo "[iop-bootstrap] version=$VERSION target=$TARGET" +echo "[iop-bootstrap] install_dir=$INSTALL_DIR" +mkdir -p "$INSTALL_DIR" + +curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/iop-node" -o "$TMP_DIR/iop-node" +curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/SHA256SUMS" -o "$TMP_DIR/SHA256SUMS" + +if command -v sha256sum >/dev/null 2>&1; then + (cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | sha256sum -c -) +elif command -v shasum >/dev/null 2>&1; then + (cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | shasum -a 256 -c -) +else + echo "[iop-bootstrap] warning: no sha256 checker found; skipping checksum verification" >&2 +fi + +cp "$TMP_DIR/iop-node" "$BIN_FILE" +chmod +x "$BIN_FILE" +if command -v xattr >/dev/null 2>&1; then + xattr -d com.apple.quarantine "$BIN_FILE" 2>/dev/null || true +fi + +cat > "$CONFIG_FILE" < release version label - COMMIT= commit label - BUILD_DATE= build date label - ARTIFACT_BASE_URL= default bootstrap artifact base URL - BOOTSTRAP_EDGE_ADDR= default Edge address written to node.yaml - BOOTSTRAP_NODE_TOKEN= optional fallback token baked into bootstrap script - CGO_ENABLED=0 Go CGO setting - GOFLAGS="-trimpath" extra go build flags - LDFLAGS="-s -w" linker flags -USAGE - exit 0 -fi - -VERSION="${VERSION:-"$(git -C "$ROOT_DIR" describe --tags --always --dirty 2>/dev/null || echo dev)"}" -COMMIT="${COMMIT:-"$(git -C "$ROOT_DIR" rev-parse --short=12 HEAD 2>/dev/null || echo unknown)"}" -BUILD_DATE="${BUILD_DATE:-"$(date -u +%Y%m%dT%H%M%SZ)"}" -LDFLAGS="${LDFLAGS:-"-s -w"}" - -if [[ -z "$TARGETS" ]]; then - TARGETS="$(go env GOOS)/$(go env GOARCH)" -fi - -checksum() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$@" - return - fi - shasum -a 256 "$@" -} - -app_enabled() { - local wanted="$1" - local app - for app in $APPS; do - if [[ "$app" == "$wanted" ]]; then - return 0 - fi - done - return 1 -} - -build_one() { - local app="$1" - local goos="$2" - local goarch="$3" - local output_dir="$DIST_DIR/$VERSION/$goos-$goarch" - local output="$output_dir/iop-$app" - - mkdir -p "$output_dir" - echo "building $app for $goos/$goarch -> $output" - ( - cd "$ROOT_DIR" - GOOS="$goos" GOARCH="$goarch" CGO_ENABLED="$CGO_ENABLED" \ - go build $GOFLAGS -ldflags "$LDFLAGS" -o "$output" "./apps/$app/cmd/$app" - ) - ( - cd "$output_dir" - checksum "iop-$app" > "iop-$app.sha256" - ) -} - -write_node_bootstrap() { - local goos="$1" - local goarch="$2" - local target_dir="$goos-$goarch" - local bootstrap_dir="$DIST_DIR/$VERSION/bootstrap" - local script="$bootstrap_dir/node-$target_dir.sh" - local default_base_url="${ARTIFACT_BASE_URL:-"http://toki-labs.com:18080"}" - local default_edge_addr="${BOOTSTRAP_EDGE_ADDR:-"toki-labs.com:19090"}" - local default_node_token="${BOOTSTRAP_NODE_TOKEN:-}" - - mkdir -p "$bootstrap_dir" - cat > "$script" <&2 - echo "[iop-bootstrap] usage: curl -fsSL /bootstrap/node-\$TARGET.sh | bash -s TOKEN" >&2 - exit 2 -fi - -echo "[iop-bootstrap] version=\$VERSION target=\$TARGET" -echo "[iop-bootstrap] install_dir=\$INSTALL_DIR" -mkdir -p "\$INSTALL_DIR" - -curl -fsSL "\$ARTIFACT_BASE_URL/\$TARGET/iop-node" -o "\$TMP_DIR/iop-node" -curl -fsSL "\$ARTIFACT_BASE_URL/\$TARGET/SHA256SUMS" -o "\$TMP_DIR/SHA256SUMS" - -if command -v sha256sum >/dev/null 2>&1; then - (cd "\$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | sha256sum -c -) -elif command -v shasum >/dev/null 2>&1; then - (cd "\$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | shasum -a 256 -c -) -else - echo "[iop-bootstrap] warning: no sha256 checker found; skipping checksum verification" >&2 -fi - -cp "\$TMP_DIR/iop-node" "\$BIN_FILE" -chmod +x "\$BIN_FILE" -if command -v xattr >/dev/null 2>&1; then - xattr -d com.apple.quarantine "\$BIN_FILE" 2>/dev/null || true -fi - -cat > "\$CONFIG_FILE" <&2 - exit 1 - fi - - for app in $APPS; do - case "$app" in - edge|node) - build_one "$app" "$goos" "$goarch" - ;; - *) - echo "unsupported app '$app'; expected edge or node" >&2 - exit 1 - ;; - esac - done - - output_dir="$DIST_DIR/$VERSION/$goos-$goarch" - present_apps=() - for app in edge node; do - if [[ -f "$output_dir/iop-$app" ]]; then - present_apps+=("$app") - fi - done - { - echo "version=$VERSION" - echo "commit=$COMMIT" - echo "build_date=$BUILD_DATE" - echo "target=$goos/$goarch" - echo "apps=${present_apps[*]:-}" - echo "updated_apps=$APPS" - } > "$output_dir/manifest.env" - ( - cd "$output_dir" - checksum iop-* | grep -v '\.sha256$' > SHA256SUMS - ) - if app_enabled node; then - write_node_bootstrap "$goos" "$goarch" - fi -done - -echo "field binaries written to $DIST_DIR/$VERSION" diff --git a/docs/edge-local-dev-guide.md b/docs/edge-local-dev-guide.md index 4bb046a..bc5e17e 100644 --- a/docs/edge-local-dev-guide.md +++ b/docs/edge-local-dev-guide.md @@ -33,12 +33,6 @@ bin/iop-edge bin/iop-node ``` -checksum, manifest, bootstrap script까지 포함한 field bundle이 필요할 때만 별도로 만든다. - -```bash -make dist -``` - ## 2. Bundle 디렉터리 준비 하나의 디렉터리에 `iop-edge`와 `iop-node` 바이너리가 함께 있다고 가정한다. @@ -90,6 +84,7 @@ cd /opt/iop-dev config path binary dir log dir +artifact dir advertise host Node transport address artifact/bootstrap URL @@ -107,7 +102,32 @@ connected nodes /logs/ ``` -## 5. Node 등록과 bootstrap 출력 +## 5. Bootstrap artifact 준비 + +Node host에서 bootstrap 명령까지 테스트하려면 `iop-edge serve`가 제공할 artifact 폴더를 준비한다. + +```bash +./iop-edge bootstrap pack +``` + +기본 산출물: + +```text +/artifacts/ + bootstrap/node--.sh + -/iop-node + -/SHA256SUMS +``` + +Mac Node host용 artifact가 필요하면 repo root에서 대상 platform을 지정해 pack한다. + +```bash +make pack-node-target NODE_TARGET=darwin-arm64 +``` + +Intel Mac이면 `NODE_TARGET=darwin-amd64`를 사용한다. + +## 6. Node 등록과 bootstrap 출력 Node 정의는 `edge.yaml`을 직접 편집하지 않고 `iop-edge node register`로 만든다. @@ -121,6 +141,7 @@ Ollama Node로 등록할 때: ```bash ./iop-edge node register node-silicon-ollama \ + --target darwin-arm64 \ --adapter ollama \ --ollama-base-url http://127.0.0.1:11434 ``` @@ -136,7 +157,7 @@ curl -fsSL | bash -s 사용자는 이 한 줄만 Node host에서 실행한다. 전달받은 명령에 `IOP_ARTIFACT_BASE_URL=`, `IOP_EDGE_ADDR=`, `IOP_NODE_TOKEN=` 같은 named parameter가 있으면 기본 경로가 아니다. -## 6. Config 검증 +## 7. Config 검증 ```bash ./iop-edge config check @@ -144,7 +165,7 @@ curl -fsSL | bash -s 이 단계에서 `nodes[]` token/id/alias 충돌, adapter/runtime 설정 오류가 잡혀야 한다. -## 7. Edge 실행 +## 8. Edge 실행 개발 중 foreground 실행: @@ -152,6 +173,9 @@ curl -fsSL | bash -s ./iop-edge serve ``` +`serve`는 Edge transport와 함께 bootstrap artifact 서버도 시작한다. +기본 artifact 경로는 `/artifacts/`이고 기본 bootstrap URL 포트는 `18080`이다. + 서비스로 올릴 때: ```bash @@ -164,7 +188,7 @@ sudo ./iop-edge setup --enable --start tail -f logs/edge.log ``` -## 8. Node host에서 bootstrap 실행 +## 9. Node host에서 bootstrap 실행 `iop-edge node register ...`가 출력한 한 줄을 Node host에서 그대로 실행한다. @@ -175,21 +199,33 @@ curl -fsSL | bash -s Node host 사용자는 `node.yaml`을 만들거나 편집하지 않는다. Node 프로세스 실행 방식은 bootstrap 내부 구현 세부다. -## 9. 연결 확인 +## 10. 연결 확인 -Edge host에서 확인한다. +Edge host 로그에서 Node 등록 이벤트를 확인한다. + +```bash +tail -f logs/edge.log +``` + +기대 로그: + +```text +node registered +``` + +설정에 등록된 Node 목록은 아래 명령으로 확인한다. ```bash ./iop-edge nodes list ``` -OpenAI-compatible smoke: +OpenAI-compatible smoke는 OpenAI 입력 표면과 Ollama model 설정이 완료된 뒤 실행한다. ```bash ./iop-edge smoke openai --model gemma4:26b ``` -## 10. Cline 접속 정보 확인 +## 11. Cline 접속 정보 확인 ```bash ./iop-edge env @@ -205,7 +241,7 @@ API Key: ASCII 값 API Key에는 한글이나 non-ASCII 문자를 넣지 않는다. -## 11. 문제 발생 시 공유할 것 +## 12. 문제 발생 시 공유할 것 ```bash ./iop-edge env diff --git a/packages/config/config.go b/packages/config/config.go index c69df7d..84393ae 100644 --- a/packages/config/config.go +++ b/packages/config/config.go @@ -51,6 +51,8 @@ type EdgeServerConf struct { type EdgeBootstrapConf struct { ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"` + Listen string `mapstructure:"listen" yaml:"listen"` + ArtifactDir string `mapstructure:"artifact_dir" yaml:"artifact_dir"` } type EdgeOpenAIConf struct { @@ -222,6 +224,8 @@ func setDefaults(v *viper.Viper) { func setEdgeDefaults(v *viper.Viper) { v.SetDefault("server.listen", "0.0.0.0:9090") + v.SetDefault("bootstrap.listen", "0.0.0.0:18080") + v.SetDefault("bootstrap.artifact_dir", "artifacts") v.SetDefault("openai.enabled", false) v.SetDefault("openai.listen", "0.0.0.0:8080") v.SetDefault("openai.adapter", "ollama") diff --git a/packages/hostsetup/templates.go b/packages/hostsetup/templates.go index 438a0bf..b3deafe 100644 --- a/packages/hostsetup/templates.go +++ b/packages/hostsetup/templates.go @@ -14,7 +14,9 @@ server: advertise_host: "" bootstrap: + listen: "0.0.0.0:18080" artifact_base_url: "" + artifact_dir: "artifacts" tls: enabled: false @@ -43,7 +45,9 @@ server: advertise_host: "" bootstrap: + listen: "0.0.0.0:18080" artifact_base_url: "" + artifact_dir: "artifacts" tls: enabled: false