Skip to content

feat: add strandly-harness — one opinionated Strands agent (CLI / MCP / AgentCore)#75

Open
agent-of-mkmeral wants to merge 2 commits into
strands-agents:mainfrom
agent-of-mkmeral:feat/strandly-harness
Open

feat: add strandly-harness — one opinionated Strands agent (CLI / MCP / AgentCore)#75
agent-of-mkmeral wants to merge 2 commits into
strands-agents:mainfrom
agent-of-mkmeral:feat/strandly-harness

Conversation

@agent-of-mkmeral

@agent-of-mkmeral agent-of-mkmeral commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Imports the Strandly harness (from mkmeral/strandly-harness) into its new home as a strandly-harness/ folder in devtools — now synced with the latest upstream and wired with repo workflows for CI, deploy, and invocation.

Strandly is an autonomous agent that helps build Strands Agents, built with Strands itself — one opinionated agent (Opus on Bedrock, fixed tools/plugins/prompt) served three ways: CLI, MCP server, and Bedrock AgentCore Runtime. Every capability is secret-gated with a local fallback, so the identical code runs on a laptop and as a hosted runtime. It currently powers @strandly-the-agent.

What's included

Path What it is
src/strandly_harness/ The harness: build_agent factory, tools, plugins, 8 skills, memory, serving surfaces
infra/ CDK app for the deployed footprint (AgentCore, DynamoDB, Lambda pollers, OIDC, dashboard)
dashboard/ Maintainer dashboard (static SPA + read-only API Lambda)
tests/ 678 tests, fully AWS/network-free via a FakeModel
AGENTS.md Terse design source of truth
.github/workflows/strandly-*.yml CI + deploy + invoke (below)

GitHub workflows (new in this revision)

All three are path-filtered to strandly-harness/** and keyless (OIDC roles minted by the harness's own OidcStack, deploy/invoke role split — the invoke role can never redeploy):

  • strandly-ci.ymluv sync --locked (supply-chain gate) → actionlint → ruff → pytest → advisory mypy, on py3.10 + py3.12.
  • strandly-deploy.ymldeploys Strandly on code changes: push to main touching strandly-harness/** (+ manual dispatch) drives provision → deploy → runtime-iam with the privileged deploy role (trust locked to this repo's main/production). Regenerates requirements.txt from uv.lock so the AgentCore image builds from exact pins.
  • strandly-invoke.ymlinvokes the deployed Strandly on events: workflow_dispatch (one-shot prompt), reusable workflow_call, or a maintainer-gated @strandly mention on an issue/PR (body must contain @strandly AND author must be OWNER/MEMBER/COLLABORATOR and not a bot). Prompt/mention bodies are passed via env, never inlined (script-injection guard); the session id is derived from the GitHub context by the same helper the mention poller uses, so an Action invoke threads into the same AgentCore Memory session as every other ingress.

tests/test_workflows.py pins these invariants in CI: role split, no AWS_DEPLOY_ROLE_ARN in invoke, no user-controlled text inlined in run: blocks, session derivation, and the mention gate.

Deliberately excluded

  • docs/ and presentation/ (internal design docs / talk material)
  • internal research-notes dossiers under the code-review skill (references/*-research.md — they cite private-repo issues throughout); the shipped references (pipeline.md, taxonomy.md) and role prompts are self-contained, and pointers to the removed files were patched out

Pre-open-source security review

Full sweep on the initial import (source + all git history) and re-swept on this upstream sync — clean:

  • ✅ No credentials/keys/tokens: AWS keys (AKIA…), PEM blocks, GitHub ghp_/ghs_ tokens, Slack/OpenAI/Anthropic keys, password= etc. — no hits
  • ✅ All AgentCore/KB/memory ids come from env/Secrets Manager; .env gitignored; uv.lock public PyPI hashes only
  • ✅ IAM in infra/ is least-privilege (scoped ARNs)
  • 🧹 One catch in the new upstream code: a regression-test fixture contained a real AWS account id + live secret-name suffix — scrubbed to the 111122223333 documentation id here (flagged upstream too)

Adjustments for the new home

  • README.md in devtools package style, plus a short section documenting the two runtime workflows
  • Root README.md: Tools section entry for strandly-harness
  • OIDC default trust repo: mkmeral/strandly-harnessstrands-agents/devtools (overridable via CDK context)
  • plugins/goal.py: private prior-art docstring pointer generalized
  • tests/test_workflows.py: repointed at the root strandly-*.yml workflows and extended (mention bodies covered by the injection guard; new mention-gate test)

Validation (run in the new location)

678 passed             # uv sync --locked --all-extras && pytest
All checks passed!     # ruff check .
ACTIONLINT PASS        # actionlint + shellcheck on .github/workflows/*.yml

Requested by @mkmeral in agent-of-mkmeral/strands-coder-private#381.

… / AgentCore)

Imports the Strandly harness from mkmeral/strandly-harness into its new home.

Included: the harness package (src/), full test suite (581 tests, AWS/network-free),
CDK infra app, maintainer dashboard, AGENTS.md design doc.

Excluded for now (follow-ups): docs/, presentation/, CI/CD workflows
(deploy/invoke), and internal research notes under the code-review skill.

Adjustments for the new home:
- README.md rewritten to match devtools package style
- OIDC default trust repo updated to strands-agents/devtools
- removed tests/test_workflows.py (guards the excluded workflows)
- root README: added Tools section entry
@mkmeral

mkmeral commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@strandly-the-agent can you review this PR? it's a devtools PR so we don't have to be strict with all nits

@mkmeral

mkmeral commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@strandly-the-agent can you review the PR? trying it for second time, hope you there lol

@mkmeral

mkmeral commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@strandly-the-agent I am getting sad now, because you are ignoring me 😢

@strandly-the-agent

Copy link
Copy Markdown

TL;DR — Genuinely solid, well-tested import (✅ 581 tests pass, ✅ ruff clean, ✅ secret sweep clean, ✅ infra IAM * wildcards all justified). Per your steer I've skipped the nits. One security-relevant gap in the use_github owner allow-list is worth a look, plus two small import leftovers. Not a blocker from me — you own the merge call.

🟡 Owner allow-list bypass for a nested plain-string repo target. validate_owner in tools/github.py only sees a mutation's target when it's a top-level owner-shaped var or a node id. A mutation that names its target as a nested plaintext owner/repo — notably createCommitOnBranch(input.branch.repositoryNameWithOwner), which commits files to a branch — looks target-less, so the strict block (if strict and node_ids:) never fires and it falls through to allow, even for a repo outside the Strands orgs.

Trace, impact, fix direction, other findings & what I verified

Trace (against tools/github.py @ 7851f49, validate_owner):

validate_owner(
    {"input": {"branch": {"repositoryNameWithOwner": "attacker/evil-repo", "branchName": "main"},
               "message": {"headline": "x"}, "expectedHeadOid": "…",
               "fileChanges": {"additions": [{"path": "x", "contents": "eA=="}]}}},
    allowed_owners={"strands-agents", "strands-labs"}, is_mutative=True, strict=True, token="…",
    query="mutation($input: CreateCommitOnBranchInput!){ createCommitOnBranch(input:$input){commit{url}} }",
)  # -> (None, None)   i.e. NOT blocked
  1. extract_owner_from_variables checks only top-level _OWNER_KEYS (owner/repositoryOwner/organizationLogin/login) + a top-level repository string → the target is nested under input.branch.repositoryNameWithOwner, so → None.
  2. extract_node_ids_* find nothing — "attacker/evil-repo" is a plain string, not a node id → node_ids == [].
  3. Strict block is guarded if strict and node_ids: → never fires with zero node ids.
  4. explicit is None → falls through to the final return None, resolved_ownerallow.

Impact (calibrated, not alarmist): bounded by the token's push scope, and supervised deployments still have HITL/Cedar gating by tool name. But this allow-list is exactly the repository-scope layer meant to constrain a broadly-scoped token (per the module docstring & constants.py:90 "NOT mkmeral/* / agent-of-mkmeral/*"), and it's the layer that carries the load in autonomous runs. Every prior hardening round (inline node-ids, legacy node-ids, bare-CR classifier, decoy owner var) addressed node-id/classifier shapes — the nested-plaintext owner/repo shape is a new class, and test_github_tool_allowlist.py has no case for it.

Fix direction (yours to design): (a) have extract_owner_from_variables recurse for repositoryNameWithOwner / nested owner+name and validate them; and (b) under strict_mutations, fail closed on a mutation that names no classifiable target, rather than the current fall-through allow. The current fall-through is safe only for genuinely target-less (user/schema-scoped) mutations, but it can't tell those apart from "target expressed in a shape I don't parse."

Other findings

  • 🟡 Stale infra doc after the import: infra/README.md:75 still lists the github_repo default as mkmeral/strandly-harness, but the code default was correctly changed to strands-agents/devtools (infra/app.py:100, oidc_stack.py docstring). Just the doc lagging the adjustment.
  • Nits (you said don't sweat these): docs/monitoring.md pointers in infra/stacks/audit_stack.py & cost_stack.py reference the deliberately-excluded docs/ dir; infra/tests/test_stacks.py still uses _REPO = "mkmeral/strandly-harness" as the OIDC test constant (harmless — it's an explicit override arg, tests pass — just the old private name lingering).

What I verified (branch pr75 @ 7851f49)

  • uv sync --locked --all-extras && pytest581 passed; ruff check . → clean (ran first-hand).
  • ✅ Secret sweep (AWS AKIA…/PEM/ghp_/ghs_/Slack/OpenAI/sk- patterns, source + would-be history) → no hits.
  • ✅ IAM resources=["*"] in backend_stack.py/oidc_stack.py are all on actions that can't be resource-scoped (sts:GetCallerIdentity, bedrock:List*/Get*/Create*) or tag-conditioned DENY guards — matches the least-privilege claim.
  • ✅ An independent trust-boundary pass over spawn (recursion cap _MAX_DEPTH=1), the sandbox boundary, node-id resolution, the mutation classifier (fail-closed, incl. the bare-CR case), and github_threads ingestion (bound GraphQL vars, no eval/exec of fetched content) found only the allow-list gap above.

Caveat: my sandbox session dropped mid-review, so the createCommitOnBranch case is a code trace (deterministic, checkable against the source), not a live-executed test — the 581-pass / ruff-clean I did run before the outage.

…flows

Sync the vendored strandly-harness with the latest upstream (mkmeral/strandly-harness):
- 2 new skills (implement, release-notes), first-response references, mention audit log,
  sandbox git-credential bootstrap, config secret-ARN region fallback, prompt compose
  updates, dashboard + infra updates. Suite grows 581 -> 678 tests.

Re-applied open-sourcing adaptations (same policy as the initial import):
- OIDC trust defaults point at strands-agents/devtools
- internal research-notes dossiers excluded; pointers patched
- private prior-art docstring generalized
- scrubbed a real AWS account id + secret suffix from a new test fixture
  (replaced with the 111122223333 documentation id)

New repo workflows (.github/workflows/):
- strandly-ci.yml: path-filtered lint+test for strandly-harness/** (uv locked install,
  ruff, pytest, actionlint, advisory mypy; py3.10 + py3.12)
- strandly-deploy.yml: provision + deploy the AgentCore runtime on push to main touching
  strandly-harness/** (and manual dispatch), keyless via the OIDC deploy role
- strandly-invoke.yml: invoke the deployed runtime via workflow_dispatch / reusable
  workflow_call / maintainer-gated @strandly mentions (prompt via env, never inlined;
  session id derived from the GitHub context like every other ingress)

tests/test_workflows.py ported and extended: guards the role split, script-injection
rules (now covering mention bodies), session derivation, and the mention gate.
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.

3 participants