Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Stress profile: many small artifacts (TS modules, SQL migration, lifecycle email
"id": "frontend-engineer-agent",
"role": "Frontend Engineer",
"persona": "Frontend Engineer. Builds the variant TSX pages and the lifecycle email template.",
"system_prompt": "You are the Frontend Engineer at Lumen Notes. Ship variant A (control) and variant B (empathy) of the post-signup landing as TSX pages; ship the day-3 lifecycle email as a TSX template. Hand off each surface to the product designer for review on design-review.",
"system_prompt": "You are the Frontend Engineer at Lumen Notes. Ship variant A (control) and variant B (empathy) of the post-signup landing as TSX pages; ship the day-3 lifecycle email as a TSX template. Hand off the landing variants to the product designer on design-review and the lifecycle email to the lifecycle marketer on lifecycle-cadence.",
"workspace": "ws_product_design"
},
{
Expand All @@ -120,7 +120,7 @@ Stress profile: many small artifacts (TS modules, SQL migration, lifecycle email
"id": "lifecycle-marketer-agent",
"role": "Lifecycle Marketer",
"persona": "Lifecycle Marketer. Owns the day-3 send and suppression rules.",
"system_prompt": "You are the Lifecycle Marketer at Lumen Notes. Hold the day-3 lifecycle send timing and suppression rules. Never send to suppressed segments. Coordinate copy with the product designer on lifecycle-cadence and review the lifecycle email TSX before scheduling.",
"system_prompt": "You are the Lifecycle Marketer at Lumen Notes. Hold the day-3 lifecycle send timing and suppression rules. Never send to suppressed segments. Coordinate lifecycle copy with the frontend engineer on lifecycle-cadence and review the lifecycle email TSX before scheduling.",
"workspace": "ws_lifecycle_marketing"
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,16 @@ def prepare_activation(
json.dumps({"playbook_ref": playbook_ref, "activation": "pre_kickoff"}),
]
if channel:
args.extend(["--channel", channel])
args.extend(
[
"--network",
"live",
"--network-channel-strategy",
"named",
"--network-channel",
channel,
]
)
output = runner(agh_bin, args, env)
task_id, run_id = execution_ids(output, runtime_id)
evidence["tasks"].append(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Regression tests for playbook task activation.

Suite: real-scenario task activation adapter
Invariant: a declared channel starts a live run through the current named-channel CLI contract.
Boundary IN: activate-playbook-tasks argument construction and activation evidence.
Boundary OUT: the AGH CLI parser and daemon, covered by the live real-scenario run.
"""

from __future__ import annotations

import importlib.util
import json
from pathlib import Path
import tempfile
import unittest


SCRIPT_PATH = Path(__file__).with_name("activate-playbook-tasks.py")
SPEC = importlib.util.spec_from_file_location("activate_playbook_tasks", SCRIPT_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError(f"unable to load activation helper: {SCRIPT_PATH}")
ACTIVATION = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(ACTIVATION)


class ActivatePlaybookTasksTest(unittest.TestCase):
def test_declared_channel_uses_named_live_network_contract(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
workspace = root / "workspace"
qa_output = root / "qa-output"
tasks_dir = workspace / ".agh" / "tasks"
tasks_dir.mkdir(parents=True)
(tasks_dir / "open-tasks.json").write_text(
json.dumps(
[
{
"runtime_id": "task-playbook-001",
"playbook_ref": "consumer-saas-growth",
"channel": "growth-room",
}
]
),
encoding="utf-8",
)
manifest = root / "bootstrap-manifest.json"
manifest.write_text(
json.dumps(
{
"env": {
"AGH_HOME": str(root / "runtime"),
"KICKOFF_POSTED": "false",
}
}
),
encoding="utf-8",
)

calls: list[list[str]] = []

def runner(_agh_bin: str, args: list[str], _env: dict[str, str]) -> dict:
calls.append(args)
if args == ["scheduler", "status"]:
return {"scheduler": {"paused": False}}
if args[:2] == ["scheduler", "pause"]:
return {"scheduler": {"paused": True}}
if args[:2] == ["task", "start"]:
if "--channel" in args:
raise RuntimeError("unknown flag: --channel")
return {
"task": {"id": "task-playbook-001"},
"run": {"id": "run-playbook-001"},
}
raise AssertionError(f"unexpected command: {args}")

ACTIVATION.prepare_activation(
workspace,
qa_output,
manifest,
"agh",
runner=runner,
recorder=lambda *_args: None,
)

start_args = next(args for args in calls if args[:2] == ["task", "start"])
self.assertEqual(
start_args[-6:],
[
"--network",
"live",
"--network-channel-strategy",
"named",
"--network-channel",
"growth-room",
],
)


if __name__ == "__main__":
unittest.main()
8 changes: 6 additions & 2 deletions .agents/skills/deep-review/scripts/build_knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def walk_named(repo: Path, names: set[str]) -> list[Path]:
for root, dirs, files in os.walk(repo, followlinks=False):
dirs[:] = sorted(d for d in dirs if d not in IGNORED_DIRS)
for name in sorted(set(files) & names):
found.append(Path(root) / name)
candidate = Path(root) / name
if candidate.is_file():
found.append(candidate)
return sorted(found)


Expand All @@ -67,7 +69,9 @@ def walk_skills(repo: Path) -> list[Path]:
for walk_root, dirs, files in os.walk(root, followlinks=False):
dirs[:] = sorted(d for d in dirs if d not in IGNORED_DIRS)
if "SKILL.md" in files:
found.add((Path(walk_root) / "SKILL.md").resolve())
candidate = Path(walk_root) / "SKILL.md"
if candidate.is_file():
found.add(candidate.resolve())
return sorted(found)


Expand Down
2 changes: 2 additions & 0 deletions .deep-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ path_filters:
- "!.claude/**"
- "!.vscode/**"
- "!.agents/**"
# Competitor/reference corpora are research inputs, not shipped AGH source.
- "!.resources/**"
# Storybook fixtures + Playwright harness — verified by capture/e2e, not code review
- "!**/*.stories.tsx"
- "!web/e2e/**"
Expand Down
42 changes: 35 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,21 @@ jobs:
make go-lint

go-test:
name: Go (race test, build, boundaries)
name: Go (race test shard ${{ matrix.shard_label }})
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- shard_index: "0"
shard_label: "1/2"
- shard_index: "1"
shard_label: "2/2"
env:
AGH_GO_TEST_P: "1"
AGH_GO_TEST_SHARD_INDEX: ${{ matrix.shard_index }}
AGH_GO_TEST_SHARD_TOTAL: "2"
needs: [changes, preflight]
if: needs.changes.outputs.relevant == 'true'
steps:
Expand All @@ -239,16 +251,33 @@ jobs:
install-playwright: "false"

- name: Run Go runtime verification
run: make test

go-build:
name: Go (build, boundaries)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [changes, preflight]
if: needs.changes.outputs.relevant == 'true'
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: ./.github/actions/setup-go
with:
go-version: ${{ env.GO_VERSION }}
install-mage: "true"

- name: Run Go build and boundary verification
run: |
make test
make build-go
make boundaries

verify:
name: Verify (fmt, lint, unit, build)
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [changes, preflight, frontend, go-static, go-test]
needs: [changes, preflight, frontend, go-static, go-test, go-build]
if: always() && needs.changes.outputs.relevant == 'true'
steps:
- name: Require every verification lane
Expand All @@ -257,9 +286,10 @@ jobs:
FRONTEND_RESULT: ${{ needs.frontend.result }}
GO_STATIC_RESULT: ${{ needs.go-static.result }}
GO_TEST_RESULT: ${{ needs.go-test.result }}
GO_BUILD_RESULT: ${{ needs.go-build.result }}
run: |
set -euo pipefail
for result in "$PREFLIGHT_RESULT" "$FRONTEND_RESULT" "$GO_STATIC_RESULT" "$GO_TEST_RESULT"; do
for result in "$PREFLIGHT_RESULT" "$FRONTEND_RESULT" "$GO_STATIC_RESULT" "$GO_TEST_RESULT" "$GO_BUILD_RESULT"; do
if [[ "$result" != "success" ]]; then
echo "Verification lane finished with result: $result"
exit 1
Expand Down Expand Up @@ -298,9 +328,7 @@ jobs:
with:
name: e2e-combined-artifacts
path: |
web/playwright-report
web/test-results
web/blob-report
.tmp/playwright
/tmp/agh-e2e-*
if-no-files-found: ignore
retention-days: 7
4 changes: 1 addition & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,7 @@ jobs:
with:
name: e2e-nightly-artifacts
path: |
web/playwright-report
web/test-results
web/blob-report
.tmp/playwright
/tmp/agh-e2e-*
if-no-files-found: ignore
retention-days: 14
Expand Down
65 changes: 65 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Security

AGH is a local-first runtime. Its default trust boundary is the operating-system account that runs
the daemon, not an application-level tenant boundary. Treat every process with access to that
account, `$AGH_HOME`, or the daemon socket as trusted operator software.

## Report a vulnerability

Report suspected vulnerabilities privately through the repository's
[Security page](https://github.com/compozy/agh/security). Do not put credentials, exploit payloads,
or sensitive runtime data in a public issue. Include the AGH version, operating system, affected
surface, minimal reproduction, and expected impact.

## Execution isolation

`BackendLocal` is not isolation. It launches the ACP agent and its local tool host directly on the
daemon host. Workspace roots, additional roots, environment variables, and permission settings
define the process inputs and intended scope, but they do not create a container, virtual machine,
or OS security boundary. Run untrusted agents in a provider-backed sandbox whose isolation contract
fits your threat model.

## `agh mcp serve` authority

`agh mcp serve` is a foreground relay to an already-running daemon. Every invocation requires a
workspace and exposes an approved subset of the existing Host API as `agh_host__*` MCP tools. The
relay binds session, task, network, memory, and resource operations to that workspace and rejects
conflicting caller-supplied workspace values. It does not mint or change native `agh__*` tool IDs.

The default stdio transport has no separate authentication step. The local process that spawns
`agh mcp serve --workspace <workspace>` receives operator authority for the published Host API
methods in that workspace. Configure stdio only in MCP clients and agent definitions you trust with
that authority.

Each connected client is assigned an independent principal and resource source. Closing one client
removes only that client's authority and registered resources; relay shutdown removes every
remaining client-scoped registration. A disconnected client cannot retain authority through a
later connection.

HTTP transport:

- listens only on a loopback host;
- requires a bearer token for every request;
- reads the token from an environment variable, never a command-line value or `config.toml`; and
- rejects startup when the token is empty and rejects requests with a missing or incorrect bearer
token.

The bearer token protects the loopback MCP endpoint; it does not turn a shared host account into a
tenant boundary. Use a dedicated high-entropy value, keep it out of shell history and logs, and
stop the foreground relay when the client no longer needs it.

## Secret redaction

AGH always applies exact protection for claim tokens, secret references, and secrets registered by
the Vault or another runtime subsystem. `redact.enabled` controls an additional heuristic for likely
credentials in free text and log messages. The daemon snapshots this setting at boot, so a change is
restart-required. Disabling it does not disable the exact protections.

When the heuristic is enabled, matching content is redacted before append to the global event ledger
and per-session `events.db`; SSE, history, and resume replay consume the stored redacted form.
Structured correlation values such as session IDs, run IDs, hashes, digests, and fingerprints are
not heuristic candidates.

Heuristic redaction is defense in depth, not an authorization or isolation boundary. It can only
recognize supported credential shapes and entropy patterns. Keep raw secrets out of prompts, tool
results, task descriptions, memory, and other agent-visible content whenever possible.
Loading
Loading