Skip to content
This repository was archived by the owner on Mar 30, 2026. It is now read-only.

feat: add lifecycle hooks to plugin manifests (AGE-203)#151

Merged
stepandel merged 6 commits intomainfrom
feat/manifest-hooks-schema
Feb 27, 2026
Merged

feat: add lifecycle hooks to plugin manifests (AGE-203)#151
stepandel merged 6 commits intomainfrom
feat/manifest-hooks-schema

Conversation

@stepandel
Copy link
Copy Markdown
Owner

@stepandel stepandel commented Feb 26, 2026

Summary

Adds lifecycle hooks to plugin manifests, enabling plugins to declare shell scripts for secret resolution, post-provisioning setup, and pre-start configuration.

Parent ticket: AGE-203

Changes

AGE-204 — Schema (commit 4ccb018)

  • Added PluginHooksSchema with resolve, postProvision, preStart hooks
  • Validation: resolve hook keys must match autoResolvable secrets
  • Added resolve hook for linearUserUuid in openclaw-linear manifest
  • 8 test cases for hooks validation

AGE-205 — Execution Engine (commit 0d4dc4d)

  • runResolveHook() — executes shell script, captures stdout, timeout enforcement
  • runLifecycleHook() — executes lifecycle scripts with streaming output
  • resolvePluginSecrets() — orchestrates all resolve hooks for a manifest
  • 12 test cases covering happy paths, timeouts, errors, env inheritance

AGE-206 — Pipeline Integration (commit 912adfd)

  • Wired hooks into cloud-init at correct lifecycle points (postProvision → workspace → resolve → preStart → gateway)
  • Replaced hardcoded Linear UUID auto-resolve with generic resolvePluginSecrets()
  • Added --skip-hooks CLI flag
  • Moved manifest-hooks to @clawup/core/manifest-hooks subpath (avoids child_process in browser bundles)

Testing

  • All 196 tests passing
  • Full build passing (core, cli, pulumi, web)

Summary by CodeRabbit

  • New Features

    • Added --skip-hooks CLI flag to optionally bypass hook execution during setup.
    • Introduced plugin lifecycle hooks (resolve, postProvision, preStart) to customize provisioning; hook scripts are run during provisioning.
    • Setup now supports automatic secret resolution via manifest hooks.
  • Tests

    • Added comprehensive tests for manifest hooks and plugin manifest validation.

Titus added 2 commits February 26, 2026 23:40
- Add PluginHooksSchema with resolve, postProvision, preStart hooks
- Add validation: resolve hook keys must match autoResolvable secrets
- Add resolve hook for linearUserUuid in openclaw-linear manifest
- Add comprehensive tests for hooks validation
- Export PluginHooksSchema from barrel
- Add runResolveHook(): executes shell script, captures stdout as resolved value
- Add runLifecycleHook(): executes postProvision/preStart scripts with streaming output
- Add resolvePluginSecrets(): orchestrates all resolve hooks for a manifest
- Timeout enforcement via AbortSignal, error handling for non-zero exits
- 12 tests covering happy paths, timeouts, errors, env inheritance
- Export all functions and types from @clawup/core
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c56046c and ccb63c2.

📒 Files selected for processing (4)
  • packages/cli/__e2e__/helpers/fixtures/plugin-identity/identity.yaml
  • packages/cli/__e2e__/helpers/fixtures/plugin-identity/plugins/test-linear.yaml
  • packages/cli/__e2e__/helpers/fixtures/plugin-identity/plugins/test-slack.yaml
  • packages/cli/__e2e__/plugins.e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/e2e/plugins.e2e.test.ts

📝 Walkthrough

Walkthrough

Adds manifest-defined plugin hooks (resolve, postProvision, preStart), implements a hook execution engine, uses hooks to auto-resolve secrets during CLI setup (opt-out via --skip-hooks), validates hooks in schemas, exposes manifest-hooks in core exports, and wires lifecycle hooks into cloud-init generation and tests.

Changes

Cohort / File(s) Summary
CLI Setup Integration
packages/cli/bin.ts, packages/cli/commands/setup.ts, packages/cli/__e2e__/plugins.e2e.test.ts
Added --skip-hooks flag; setup now invokes manifest-hook-based secret resolution (unless skipped); updated e2e fixtures/tests to expect test-prefixed plugin labels and resolved LINEAR_USER_UUID behavior.
Manifest Hook Engine
packages/core/src/manifest-hooks.ts, packages/core/src/plugin-registry.ts
New manifest hook runner: runResolveHook, runLifecycleHook, resolvePluginSecrets with timeouts, env propagation, and detailed errors; added Linear resolve hook entry in plugin registry.
Schema & Validation
packages/core/src/schemas/plugin-manifest.ts, packages/core/src/schemas/index.ts, packages/core/src/index.ts
Introduced PluginHooksSchema; extended PluginManifestSchema with optional hooks and added validation ensuring resolve keys map to existing autoResolvable secrets; re-exported hooks schema.
Pulumi / cloud-init
packages/pulumi/src/components/cloud-init.ts, packages/pulumi/src/index.ts
PluginInstallConfig now carries hooks; cloud-init generation runs postProvision and preStart hooks at appropriate injection points with environment setup.
Tests & Fixtures
packages/core/src/__tests__/manifest-hooks.test.ts, packages/core/src/__tests__/plugin-manifest.test.ts, packages/cli/__e2e__/helpers/fixtures/plugin-identity/*, packages/cli/__e2e__/helpers/fixtures/plugin-identity/plugins/*
Added comprehensive tests for hook execution and manifest validation; added test plugin fixtures (test-linear, test-slack) and updated identity fixture for e2e expectations.
Package Exports
packages/core/package.json
Exported new public module ./manifest-hooks and added corresponding typesVersions mapping.

Sequence Diagram

sequenceDiagram
    autonumber
    actor User
    participant CLI as Setup Command
    participant Registry as Plugin Registry
    participant Engine as Hook Engine
    participant Shell as Shell/Process
    participant CloudInit as Cloud-init generator

    User->>CLI: run setup (no --skip-hooks)
    CLI->>Registry: fetch plugin manifests
    Registry-->>CLI: manifests (may include hooks)
    CLI->>Engine: resolvePluginSecrets(manifest, env)
    Engine->>Shell: execute resolve hook script (with env)
    Shell-->>Engine: stdout (resolved value) / error
    Engine-->>CLI: resolved secrets map or error
    CLI->>CloudInit: build plugins for agent (includes hooks)
    CloudInit->>Shell: execute postProvision / preStart hooks during provisioning
    Shell-->>CloudInit: streamed output / exit status
    CLI-->>User: setup complete (with resolved secrets applied)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 I nibbled at manifests under moonlight clear,

Hooks whispered secrets that once were dear.
CLI hops, resolves, then binds with care,
Cloud-init hums, lifecycle scripts prepare.
A tiny rabbit cheers: "Run hooks — bring cheer!"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add lifecycle hooks to plugin manifests (AGE-203)' directly and accurately reflects the main objective of the PR, which is to introduce lifecycle hooks to plugin manifests.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/manifest-hooks-schema

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Wire postProvision/preStart hooks into cloud-init at correct lifecycle points
- Replace hardcoded Linear UUID auto-resolve with generic resolvePluginSecrets()
- Add --skip-hooks CLI flag to bypass hook execution during setup
- Pass hooks through Pulumi plugin config to cloud-init generation
- Move manifest-hooks to @clawup/core/manifest-hooks subpath (avoids child_process in browser)
- Full build passing (core, cli, pulumi, web), all 196 tests green
@stepandel stepandel changed the title feat: add hooks and secrets fields to PluginManifest schema (AGE-204) feat: add lifecycle hooks to plugin manifests (AGE-203) Feb 26, 2026
@stepandel
Copy link
Copy Markdown
Owner Author

🔍 QA Approved

Build: ✅ Clean
Unit tests: ✅ 196/196 (20 new — 8 schema validation, 12 hook execution)

Verified against AGE-203 acceptance criteria:

  • AGE-204 (Schema): PluginHooksSchema with resolve/postProvision/preStart ✅
  • AGE-205 (Execution engine): runResolveHook, runLifecycleHook, resolvePluginSecrets with timeout enforcement, error handling, env inheritance ✅
  • AGE-206 (Pipeline integration): Hooks wired into cloud-init at correct lifecycle points, hardcoded Linear UUID logic replaced with generic resolvePluginSecrets(), --skip-hooks CLI flag ✅

Tests cover: happy paths, timeouts, non-zero exits, empty output, env var inheritance, fail-fast on hook errors. Looks solid.

@stepandel
Copy link
Copy Markdown
Owner Author

🔍 QA Verified — All 3 sub-tickets reviewed and tested (AGE-204, AGE-205, AGE-206).

Build: ✅ green
Tests: ✅ 196/196 passing (including 12 new manifest-hooks tests + 8 new schema validation tests)

Review notes:

  • Schema: PluginHooksSchema with resolve, postProvision, preStart — validation correctly enforces resolve keys match autoResolvable secrets
  • Execution engine: runResolveHook, runLifecycleHook, resolvePluginSecrets — timeout enforcement, error handling, and fail-fast behavior all verified
  • Integration: cloud-init wiring follows correct lifecycle order, --skip-hooks flag works, hardcoded Linear UUID fetch cleanly replaced with generic hook system
  • Backward compat: existing manifests without hooks load fine

Ready to merge.

Base automatically changed from feat/plugin-abstraction to main February 27, 2026 00:25
Scout added 2 commits February 27, 2026 00:26
- Merge main (includes PR #148 plugin abstraction)
- Fix plugin E2E test: skip hooks during setup (fake API keys can't resolve)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/commands/setup.ts (1)

406-406: ⚠️ Potential issue | 🟡 Minor

Variable shadowing: manifest shadows outer scope variable.

The variable manifest on line 406 shadows the manifest variable declared on line 86 (the parsed ClawupManifest). Consider renaming to pluginManifest for clarity, consistent with the rename on line 306.

🔧 Proposed fix
-        const manifest = resolvePlugin(pluginName, fi.identityResult);
+        const pluginManifest = resolvePlugin(pluginName, fi.identityResult);
-        for (const [key, secret] of Object.entries(manifest.secrets)) {
+        for (const [key, secret] of Object.entries(pluginManifest.secrets)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/commands/setup.ts` at line 406, The local const named manifest
returned from resolvePlugin(pluginName, fi.identityResult) shadows the outer
ClawupManifest variable named manifest; rename this local variable to
pluginManifest (or the same identifier used earlier on line 306) and update any
subsequent references in the same block to pluginManifest so there is no
shadowing and the outer parsed ClawupManifest remains unambiguous (changes
pertain to the resolvePlugin call and its immediate uses).
🧹 Nitpick comments (3)
packages/pulumi/src/components/cloud-init.ts (1)

177-207: Hook script generation looks correct; minor note on HEREDOC delimiter safety.

The scripts properly set up the NVM environment and run hooks as the ubuntu user. The lifecycle order (postProvision before workspace, preStart after config) matches the documented intent.

One consideration: the HEREDOC delimiter (e.g., HOOK_POST_PROVISION_OPENCLAW_LINEAR) is derived from the plugin name. If a hook script ever contained this exact string on a line by itself, it would cause early termination. This is unlikely in practice but could be mitigated by using a more unique delimiter pattern (e.g., appending a random suffix or hash).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/pulumi/src/components/cloud-init.ts` around lines 177 - 207, The
HEREDOC delimiters used in postProvisionHooksScript and preStartHooksScript
(e.g., HOOK_POST_PROVISION_${p.name.toUpperCase().replace(/-/g, "_")} and
HOOK_PRE_START_...) are derived from plugin names and could accidentally collide
with hook content; change the delimiter generation to include a deterministic
unique suffix (for example a short hash of p.name or a timestamp/nonce) and use
that same unique token for both the opening and closing delimiter; update the
delimiter expressions in postProvisionHooksScript and preStartHooksScript (and
their corresponding HOOK_POST_PROVISION_... / HOOK_PRE_START_... closers) so
each heredoc uses a unique, collision-resistant identifier.
packages/cli/commands/setup.ts (1)

335-347: Consider extracting envVar-to-camelCase conversion to a utility.

This conversion logic is duplicated in multiple places: here (lines 338-342), in resolveIsSecret (lines 587-592), and in getValidatorHint (lines 615-619). Extracting it to a shared utility would improve maintainability.

♻️ Suggested utility function
// In lib/env.ts or similar
export function envVarToCamelCase(envVar: string): string {
  return envVar
    .toLowerCase()
    .split("_")
    .map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
    .join("");
}

Then use it in setup.ts:

-          const envDerivedKey = sec.envVar
-            .toLowerCase()
-            .split("_")
-            .map((part: string, i: number) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))
-            .join("");
+          const envDerivedKey = envVarToCamelCase(sec.envVar);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/commands/setup.ts` around lines 335 - 347, Extract the repeated
env var -> camelCase logic into a shared utility function (e.g.
envVarToCamelCase(envVar: string)) and replace the inline implementations inside
the loop that maps agentSecrets to hookEnv (references: agentSecrets,
pluginManifest.secrets, hookEnv) as well as the uses in resolveIsSecret and
getValidatorHint to call this new function; ensure the utility lowercases,
splits on "_" and camelCases subsequent segments as shown in the suggested
snippet and export it from a common module (e.g. lib/env) so all three call
sites import and reuse it.
packages/core/src/__tests__/manifest-hooks.test.ts (1)

1-3: Remove unused imports.

vi, beforeEach, and afterEach are imported but never used in this file.

🧹 Proposed fix
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { describe, it, expect } from "vitest";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/__tests__/manifest-hooks.test.ts` around lines 1 - 3, The
test file imports unused symbols vi, beforeEach, and afterEach from "vitest";
update the import statement in manifest-hooks.test.ts to remove those unused
imports so only the used symbols (describe, it, expect) are imported, e.g., edit
the import line that currently references vi, beforeEach, and afterEach to only
import describe, it, and expect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/commands/setup.ts`:
- Line 24: Remove the unused import runLifecycleHook from the import statement
that also brings in resolvePluginSecrets (the import line containing
resolvePluginSecrets and runLifecycleHook); update the import to only import
resolvePluginSecrets so the symbol runLifecycleHook is no longer imported or
referenced in packages/cli/commands/setup.ts and ensure no other usages of
runLifecycleHook exist in the file.

---

Outside diff comments:
In `@packages/cli/commands/setup.ts`:
- Line 406: The local const named manifest returned from
resolvePlugin(pluginName, fi.identityResult) shadows the outer ClawupManifest
variable named manifest; rename this local variable to pluginManifest (or the
same identifier used earlier on line 306) and update any subsequent references
in the same block to pluginManifest so there is no shadowing and the outer
parsed ClawupManifest remains unambiguous (changes pertain to the resolvePlugin
call and its immediate uses).

---

Nitpick comments:
In `@packages/cli/commands/setup.ts`:
- Around line 335-347: Extract the repeated env var -> camelCase logic into a
shared utility function (e.g. envVarToCamelCase(envVar: string)) and replace the
inline implementations inside the loop that maps agentSecrets to hookEnv
(references: agentSecrets, pluginManifest.secrets, hookEnv) as well as the uses
in resolveIsSecret and getValidatorHint to call this new function; ensure the
utility lowercases, splits on "_" and camelCases subsequent segments as shown in
the suggested snippet and export it from a common module (e.g. lib/env) so all
three call sites import and reuse it.

In `@packages/core/src/__tests__/manifest-hooks.test.ts`:
- Around line 1-3: The test file imports unused symbols vi, beforeEach, and
afterEach from "vitest"; update the import statement in manifest-hooks.test.ts
to remove those unused imports so only the used symbols (describe, it, expect)
are imported, e.g., edit the import line that currently references vi,
beforeEach, and afterEach to only import describe, it, and expect.

In `@packages/pulumi/src/components/cloud-init.ts`:
- Around line 177-207: The HEREDOC delimiters used in postProvisionHooksScript
and preStartHooksScript (e.g.,
HOOK_POST_PROVISION_${p.name.toUpperCase().replace(/-/g, "_")} and
HOOK_PRE_START_...) are derived from plugin names and could accidentally collide
with hook content; change the delimiter generation to include a deterministic
unique suffix (for example a short hash of p.name or a timestamp/nonce) and use
that same unique token for both the opening and closing delimiter; update the
delimiter expressions in postProvisionHooksScript and preStartHooksScript (and
their corresponding HOOK_POST_PROVISION_... / HOOK_PRE_START_... closers) so
each heredoc uses a unique, collision-resistant identifier.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c92c9f and c56046c.

📒 Files selected for processing (13)
  • packages/cli/__e2e__/plugins.e2e.test.ts
  • packages/cli/bin.ts
  • packages/cli/commands/setup.ts
  • packages/core/package.json
  • packages/core/src/__tests__/manifest-hooks.test.ts
  • packages/core/src/__tests__/plugin-manifest.test.ts
  • packages/core/src/index.ts
  • packages/core/src/manifest-hooks.ts
  • packages/core/src/plugin-registry.ts
  • packages/core/src/schemas/index.ts
  • packages/core/src/schemas/plugin-manifest.ts
  • packages/pulumi/src/components/cloud-init.ts
  • packages/pulumi/src/index.ts

resolvePlugins,
PLUGIN_MANIFEST_REGISTRY,
} from "@clawup/core";
import { resolvePluginSecrets, runLifecycleHook } from "@clawup/core/manifest-hooks";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if runLifecycleHook is actually used in setup.ts
rg -n "runLifecycleHook" packages/cli/commands/setup.ts

# Also check file size to understand scope
wc -l packages/cli/commands/setup.ts

Repository: stepandel/clawup

Length of output: 183


Remove unused runLifecycleHook import from line 24.

The function is imported but never used in this file. Only resolvePluginSecrets from the same import is actually called (line 351).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/commands/setup.ts` at line 24, Remove the unused import
runLifecycleHook from the import statement that also brings in
resolvePluginSecrets (the import line containing resolvePluginSecrets and
runLifecycleHook); update the import to only import resolvePluginSecrets so the
symbol runLifecycleHook is no longer imported or referenced in
packages/cli/commands/setup.ts and ensure no other usages of runLifecycleHook
exist in the file.

- Created test-linear and test-slack plugin manifests in fixture identity
  with echo-based resolve hooks (no real API calls)
- test-linear hook: resolves linearUserUuid via echo instead of curl to Linear API
- Plugin E2E tests now exercise full hook resolution pipeline
- Removed skipHooks workaround — hooks run end-to-end
- Verify hook-resolved UUID appears in deployed container's cloud-init script
- All 24 E2E tests passing, 196 unit tests passing
@stepandel stepandel merged commit 935a719 into main Feb 27, 2026
5 checks passed
@stepandel stepandel deleted the feat/manifest-hooks-schema branch February 27, 2026 00:46
@stepandel
Copy link
Copy Markdown
Owner Author

🔍 QA Review — AGE-208 onboard hook type

Build: ✅ green
Tests: ✅ 206/206 passing (10 new tests: 4 schema + 5 execution + 1 edge case)

Issue found — runOnce secret check uses wrong keys:

In setup.ts lines ~394-401, the runOnce check iterates pluginManifest.secrets entries (raw keys like apiKey, webhookSecret) and looks them up in resolvedSecrets.perAgent[agentName], which uses envVar-derived camelCase keys (linearApiKey, linearWebhookSecret). The key mismatch means allSecretsPresent will always be false, causing onboard hooks to run even when all secrets are present.

Fix: Map the raw secret key through the envVar → camelCase conversion before lookup, e.g.:

const envDerivedKey = sec.envVar.toLowerCase().split('_').map((p, i) => i === 0 ? p : p[0].toUpperCase() + p.slice(1)).join('');
return agentSecrets[envDerivedKey] || autoResolvedSecrets[fi.agent.role]?.[key];

Everything else looks solid — schema, execution engine, CLI flag, interactive prompts, and tests are all well done. Requesting changes for this one fix.

stepandel added a commit that referenced this pull request Feb 27, 2026
* feat: add hooks field to PluginManifestSchema (AGE-204)

- Add PluginHooksSchema with resolve, postProvision, preStart hooks
- Add validation: resolve hook keys must match autoResolvable secrets
- Add resolve hook for linearUserUuid in openclaw-linear manifest
- Add comprehensive tests for hooks validation
- Export PluginHooksSchema from barrel

* feat: implement manifest hook execution engine (AGE-205)

- Add runResolveHook(): executes shell script, captures stdout as resolved value
- Add runLifecycleHook(): executes postProvision/preStart scripts with streaming output
- Add resolvePluginSecrets(): orchestrates all resolve hooks for a manifest
- Timeout enforcement via AbortSignal, error handling for non-zero exits
- 12 tests covering happy paths, timeouts, errors, env inheritance
- Export all functions and types from @clawup/core

* feat: integrate manifest hooks into provisioning pipeline (AGE-206)

- Wire postProvision/preStart hooks into cloud-init at correct lifecycle points
- Replace hardcoded Linear UUID auto-resolve with generic resolvePluginSecrets()
- Add --skip-hooks CLI flag to bypass hook execution during setup
- Pass hooks through Pulumi plugin config to cloud-init generation
- Move manifest-hooks to @clawup/core/manifest-hooks subpath (avoids child_process in browser)
- Full build passing (core, cli, pulumi, web), all 196 tests green

* merge: main into feat/manifest-hooks-schema

- Merge main (includes PR #148 plugin abstraction)
- Fix plugin E2E test: skip hooks during setup (fake API keys can't resolve)

* feat: test hooks E2E with echo-based stub manifests

- Created test-linear and test-slack plugin manifests in fixture identity
  with echo-based resolve hooks (no real API calls)
- test-linear hook: resolves linearUserUuid via echo instead of curl to Linear API
- Plugin E2E tests now exercise full hook resolution pipeline
- Removed skipHooks workaround — hooks run end-to-end
- Verify hook-resolved UUID appears in deployed container's cloud-init script
- All 24 E2E tests passing, 196 unit tests passing

---------

Co-authored-by: Titus <stepan.arsentjev+titus@gmail.com>
Co-authored-by: Scout <scout@openclaw.ai>
stepandel added a commit that referenced this pull request Feb 27, 2026
…Codex agent (#111)

* feat(core): expand MODEL_PROVIDERS with OpenAI, Google, and OpenRouter

- Add openai, google, openrouter entries to MODEL_PROVIDERS
- Add KEY_INSTRUCTIONS for each provider's API key
- Add getProviderForModel() helper to extract provider from model strings
- Add comprehensive unit tests for new constants and helper

Closes AGE-148

* feat: centralize plugin metadata into enriched registry manifests

Replace 15+ hardcoded plugin-specific locations across 8+ files with a
centralized plugin manifest system. All plugin knowledge (secrets, config
paths, internal keys, transforms, webhook setup, validators, instructions)
now lives in enriched registry entries that consumers read from instead of
hardcoding plugin-specific logic.

Key changes:
- Add PluginManifest Zod schema with full metadata (secrets with scope/
  isSecret/autoResolvable/validator, configPath, internalKeys,
  configTransforms, webhookSetup)
- Create plugin-loader with resolvePlugin() resolution chain and generic
  fallback for unknown plugins
- Generalize config-generator to use configPath-driven code paths instead
  of name-based branching
- Make buildManifestSecrets, validators, KNOWN_SECRETS, webhook setup,
  auto-resolve, and post-deploy messages all data-driven from the registry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add identity-bundled plugin manifests with 3-tier resolution

Plugin resolution now checks: built-in registry > identity-bundled
manifests (from plugins/ dir in identity repos) > generic fallback.
This lets third-party identity repos ship their own plugin metadata
without requiring changes to the built-in registry.

- Add standalone example manifests for Linear and Slack plugins
- Add pluginManifests field to IdentityResult
- Scan plugins/ directory in identity repos for YAML plugin manifests
- Thread IdentityResult through all resolvePlugin() call sites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(pulumi): provider-aware config generation (#113)

- config-generator uses MODEL_PROVIDERS to set correct env var per provider
- Anthropic OAuth detection preserved only for Anthropic provider
- cloud-init exports correct env var name for non-Anthropic providers
- CloudInitConfig gains optional modelProvider field

Closes AGE-150

Co-authored-by: Titus <titus@openclaw.ai>

* feat(core): add Codex to coding agent registry (#114)

- Add 'codex' entry to CODING_AGENT_REGISTRY with install script, model config, and cliBackend
- Install via npm install -g @openai/codex
- Config stored in ~/.codex/config.toml
- Uses exec --full-auto for one-shot execution

Closes AGE-151

Co-authored-by: Titus <titus@openclaw.ai>

* feat(cli): dynamic model/provider selection in init command (#112)

- Replace hardcoded Anthropic API key collection with provider picker
- Show models from MODEL_PROVIDERS for selected provider
- Free-form model input for providers with empty model lists (OpenRouter)
- Provider-specific API key validation and instructions
- Store modelProvider, defaultModel, and provider env var in Pulumi config
- Backward compatible: selecting Anthropic produces equivalent behavior

Closes AGE-149

Co-authored-by: Titus <titus@openclaw.ai>

* fix: address CodeRabbit review comments and add plugin E2E tests

Phase 1 — Review fixes:
- Use plugin.enabled for channel-level enabled flag in config-generator
- Respect transform.removeSource before skipping source key
- Log warnings instead of swallowing identity/plugin resolution errors
- Namespace buildKnownSecrets/buildValidators keys by envVar-derived
  camelCase to prevent collisions when plugins share raw key names
- Fix configJsonPath from "plugins.entries.linear" to
  "plugins.entries.openclaw-linear" in both registry and YAML example
- Add superRefine validation that webhookSetup.secretKey exists in secrets
- Use unique per-plugin webhook output keys (${role}${PluginSlug}WebhookUrl)
- Build validators from full resolved manifests, not just static registry
- Fix jqPath construction (was a no-op replace, now strips leading dot)
- Use secret.envVar for Pulumi key derivation instead of plugin.displayName
- Add AbortController timeout, HTTP status check, and GraphQL error
  handling to Linear API call in setup
- Log warnings for invalid identity-bundled plugin manifests
- Resolve isSecret by agent's plugin context instead of scanning all
  manifests by raw key name
- Scope auto-resolvable checks in env.ts to agent's actual plugins

Phase 2 — E2E tests (57 new tests):
- plugin-loader.test.ts: resolution, secrets, known secrets, validators
- plugin-manifest.test.ts: schema validation, webhook secretKey validation
- plugin-e2e.test.ts: end-to-end integration for Linear, Slack, multi-plugin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add plugin deployment E2E tests for Slack and Linear

Add E2E tests that verify Slack and Linear plugin configuration through
the full deploy → validate → destroy lifecycle using local Docker
containers. Includes a new plugin-identity fixture with both plugins
enabled, and extends createTestProject with options for custom identity
directories, agent names, roles, and extra env lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve autoResolvable lookup mismatch in env.ts

The auto-resolvable check was using envVar-derived camelCase keys (e.g.,
linearApiKey) to index pm.secrets, but manifest secrets use raw key names
(e.g., apiKey). Fixed to match via envVar instead of raw key lookup.

* feat: add hooks field to PluginManifestSchema (AGE-204)

- Add PluginHooksSchema with resolve, postProvision, preStart hooks
- Add validation: resolve hook keys must match autoResolvable secrets
- Add resolve hook for linearUserUuid in openclaw-linear manifest
- Add comprehensive tests for hooks validation
- Export PluginHooksSchema from barrel

* feat: implement manifest hook execution engine (AGE-205)

- Add runResolveHook(): executes shell script, captures stdout as resolved value
- Add runLifecycleHook(): executes postProvision/preStart scripts with streaming output
- Add resolvePluginSecrets(): orchestrates all resolve hooks for a manifest
- Timeout enforcement via AbortSignal, error handling for non-zero exits
- 12 tests covering happy paths, timeouts, errors, env inheritance
- Export all functions and types from @clawup/core

* feat: integrate manifest hooks into provisioning pipeline (AGE-206)

- Wire postProvision/preStart hooks into cloud-init at correct lifecycle points
- Replace hardcoded Linear UUID auto-resolve with generic resolvePluginSecrets()
- Add --skip-hooks CLI flag to bypass hook execution during setup
- Pass hooks through Pulumi plugin config to cloud-init generation
- Move manifest-hooks to @clawup/core/manifest-hooks subpath (avoids child_process in browser)
- Full build passing (core, cli, pulumi, web), all 196 tests green

* feat: rework multi-provider model selection for current architecture (AGE-207)

- Merge feat/plugin-abstraction + feat/manifest-hooks-schema into PR #111 branch
- Resolve init.ts conflicts (keep manifest-only init, no model selection UI)
- Add modelProvider/defaultModel to ClawupManifestSchema
- Wire provider-aware API key handling in setup.ts (replaces hardcoded Anthropic)
- Wire modelProvider through Pulumi shared component to cloud-init
- Fix Codex coding agent missing secrets field
- Add OpenAI/Google/OpenRouter validation hints in setup.ts
- All 209 tests passing, full build green

* fix: resolve workspace mock, env, and Pulumi backend issues in E2E tests

- Add workspace module mocks to all E2E tests (isDevMode, getWorkspaceDir, ensureWorkspace)
- Add PLUGINTESTER_LINEAR_USER_UUID to bypass API auto-resolve
- Fix error-cases expectations (TestCancelError → ProcessExitError for deploy/destroy cancellation)
- Add PULUMI_BACKEND_URL=file://~ to ensure local backend is used

* fix: use project mode for E2E tests and handle manifest path in workspace

- Change workspace mocks to use project mode (tempDir/.clawup) instead of dev mode
- Update Pulumi program to check parent dir for clawup.yaml (project mode support)
- Add workspace setup in E2E test beforeAll to copy Pulumi files
- Rebuild Pulumi dist with updated manifest resolution

* fix: proper workspace setup with matching path structure for E2E tests

- Fix repoRoot path resolution (../../.. from __e2e__ dir)
- Create packages/pulumi/dist structure in workspace to match Pulumi.yaml main path
- Symlink node_modules to avoid ESM resolution issues
- Apply to lifecycle, plugins, and redeploy (both describe blocks)

* fix: search upward for clawup.yaml in Pulumi program

Pulumi sets CWD to the program directory (packages/pulumi/dist/),
not the Pulumi.yaml directory. Walk up to find clawup.yaml.
Also add package-directory dependency for Pulumi ESM support.

* fix: configurable local base port for E2E tests (avoid gateway port conflict)

Add CLAWUP_LOCAL_BASE_PORT env var to override the default 18789 base port
for local Docker deployments. E2E tests use 28789 to avoid conflicts with
the running OpenClaw gateway.

* fix: add retry for openclaw.json read in plugin E2E test

Container cloud-init takes a moment to write openclaw.json.
Retry up to 10 times with 1s delay.

* fix: verify plugin config via container env vars instead of openclaw.json

The container's cloud-init doesn't create openclaw.json in E2E test
environments. Verify plugin secrets are passed as env vars instead.

* fix: verify plugin secrets in cloud-init script instead of env vars

Secrets are embedded in base64-encoded CLOUDINIT_SCRIPT, not as
direct Docker env vars.

* ci: add GitHub Actions CI workflow for unit + E2E tests

- Unit tests run on push/PR to main
- E2E tests run after unit tests pass (needs Docker, Pulumi)
- Lint/typecheck job runs in parallel
- Concurrency: cancel in-progress runs on same ref

* fix: address CodeRabbit review comments (round 3)

- CI: use explicit backend path instead of file://~ (no tilde expansion in GHA)
- CI: seed AGENT_LINEAR_USER_UUID for deterministic E2E
- CI: pulumi login uses PULUMI_BACKEND_URL for consistency
- E2E: clean up all env vars in afterAll (prevent state leakage)
- E2E: fix misleading 'dev mode' comments (tests use project mode)

* fix: stabilize cancellation tests for CI compatibility

- Accept both TestCancelError and ProcessExitError in cancel tests
  (deploy tool may propagate either depending on environment)
- Create Pulumi backend directory before login in CI
- Fix: mkdir .pulumi-state before pulumi login

* fix: address CodeRabbit review comments (round 4)

- CI: add explicit permissions (contents: read) for least privilege
- CI: pin Pulumi CLI to v3.223.0 for deterministic builds
- E2E: isolate Pulumi backend per suite (file://<tempDir>/.pulumi-backend)
- E2E: save/restore env vars instead of delete (prevents state leakage)
- E2E: add tempDir guard to getWorkspaceDir mocks in all test files

* feat: add lifecycle hooks to plugin manifests (AGE-203) (#151)

* feat: add hooks field to PluginManifestSchema (AGE-204)

- Add PluginHooksSchema with resolve, postProvision, preStart hooks
- Add validation: resolve hook keys must match autoResolvable secrets
- Add resolve hook for linearUserUuid in openclaw-linear manifest
- Add comprehensive tests for hooks validation
- Export PluginHooksSchema from barrel

* feat: implement manifest hook execution engine (AGE-205)

- Add runResolveHook(): executes shell script, captures stdout as resolved value
- Add runLifecycleHook(): executes postProvision/preStart scripts with streaming output
- Add resolvePluginSecrets(): orchestrates all resolve hooks for a manifest
- Timeout enforcement via AbortSignal, error handling for non-zero exits
- 12 tests covering happy paths, timeouts, errors, env inheritance
- Export all functions and types from @clawup/core

* feat: integrate manifest hooks into provisioning pipeline (AGE-206)

- Wire postProvision/preStart hooks into cloud-init at correct lifecycle points
- Replace hardcoded Linear UUID auto-resolve with generic resolvePluginSecrets()
- Add --skip-hooks CLI flag to bypass hook execution during setup
- Pass hooks through Pulumi plugin config to cloud-init generation
- Move manifest-hooks to @clawup/core/manifest-hooks subpath (avoids child_process in browser)
- Full build passing (core, cli, pulumi, web), all 196 tests green

* merge: main into feat/manifest-hooks-schema

- Merge main (includes PR #148 plugin abstraction)
- Fix plugin E2E test: skip hooks during setup (fake API keys can't resolve)

* feat: test hooks E2E with echo-based stub manifests

- Created test-linear and test-slack plugin manifests in fixture identity
  with echo-based resolve hooks (no real API calls)
- test-linear hook: resolves linearUserUuid via echo instead of curl to Linear API
- Plugin E2E tests now exercise full hook resolution pipeline
- Removed skipHooks workaround — hooks run end-to-end
- Verify hook-resolved UUID appears in deployed container's cloud-init script
- All 24 E2E tests passing, 196 unit tests passing

---------

Co-authored-by: Titus <stepan.arsentjev+titus@gmail.com>
Co-authored-by: Scout <scout@openclaw.ai>

* fix: use printf instead of echo -n in resolve hook test

On macOS, /bin/sh in POSIX mode doesn't support echo -n and
literally outputs "-n", causing the empty-output test to pass
when it should fail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump CLI to v2.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: fail fast on unknown model provider instead of silent fallback

- cloud-init.ts: throw on unknown modelProvider instead of degrading to MODEL_API_KEY
- config-generator.ts: validate provider key with explicit error, remove unsafe assertion

* test: add comprehensive test coverage for multi-provider changes

Unit tests (23 new, 232 total):
- cloud-init-providers.test.ts: 7 tests — verifies env var export for
  Anthropic (default + explicit), OpenAI, Google, OpenRouter, unknown
  provider error, and undefined modelProvider fallback
- config-generator-providers.test.ts: 8 tests — verifies Python config
  patching for all providers, unknown provider error, model string in
  config, backup model, and Codex coding agent CLI backend
- coding-agent-registry.test.ts: 8 tests — verifies Codex entry
  (secrets, install script, configureModelScript, cliBackend)

E2E enhancements:
- lifecycle deploy test now verifies CLOUDINIT_SCRIPT contains Anthropic
  auto-detect logic (default provider)
- test-project helper supports model/modelProvider overrides

---------

Co-authored-by: Titus <titus@openclaw.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Scout <scout@openclaw.ai>
Co-authored-by: Titus <stepan.arsentjev+titus@gmail.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant