Module 07 · Collaboration at Scale
- Explain the difference between a fork and a clone, and when each is used
- Configure upstream and origin remotes and keep a fork in sync
- Navigate CONTRIBUTING.md and CODE_OF_CONDUCT.md as an incoming contributor
- Use the GitHub CLI (gh) to create issues, open PRs, and manage reviews without leaving the terminal
- Propose a new Specialist Agent following the project's contribution process end-to-end
- Describe what security-aware review of a third-party AI agent contribution looks like
Background
Every module so far assumed you are the repository owner — you configured it, built the CI pipeline, and set the branch protection rules. That’s one role on a real project. The other role — the one most developers spend more time in — is contributor: someone proposing changes to a project they don’t own.
Open-source contribution has a specific workflow designed around this asymmetry. You don’t have write access to the upstream repository, so you can’t push branches directly. Instead, you fork, work in your fork, and propose changes back via a Pull Request. The upstream maintainers review and decide whether to merge.
This module puts you in the contributor seat. You’ll read the project’s
CONTRIBUTING.md the way a new contributor does, follow the process it
describes, and use the GitHub CLI to handle the mechanics without touching
the browser. By the end, you’ll have submitted a complete agent contribution
through the same process any external contributor would use.
The Calculate Agent has been tracked as an Issue since Module 04 — it was registered in the Orchestrator in Module 02 but never implemented. In this module you’ll close that loop by contributing the implementation through the full open-source process: read the contributor docs, open an issue (if one doesn’t exist), fork, implement, open a PR upstream, and respond to review. You’ll also sync your fork after a simulated upstream change lands while your PR is open — the fork drift scenario every contributor eventually encounters.
Concepts
Forks, Remotes, and the Two-Remote Model
When you work on a project you don’t own, you have two Git remotes:
GitHub (upstream) GitHub (your fork) Your machine───────────────── ────────────────── ─────────────fischer3-net/ [YOUR-USERNAME]/ local clonegit-github-security-learning ← git-github-security-learning ← (working directory) │ │ │ git fetch upstream │ git push origin │ ◀────────────────────── │ ──────────────────▶ │ │ └─── maintainer merges PR ─┘| Remote | What it points to | Used for |
|---|---|---|
origin | Your fork on GitHub | Pushing your branches |
upstream | The original repository | Fetching the latest main |
You never push to upstream — you don’t have write access. You push to
origin (your fork), then open a PR from your fork to upstream.
Configuring Remotes
# Clone your fork (origin is set automatically)git clone https://github.com/YOUR-USERNAME/git-github-security-learning.gitcd git-github-security-learning
# Add the upstream remotegit remote add upstream https://github.com/fischer3-net/git-github-security-learning.git
# Verify both remotes existgit remote -v# origin https://github.com/YOUR-USERNAME/git-github-security-learning.git (fetch)# origin https://github.com/YOUR-USERNAME/git-github-security-learning.git (push)# upstream https://github.com/fischer3-net/git-github-security-learning.git (fetch)# upstream https://github.com/fischer3-net/git-github-security-learning.git (push)Keeping Your Fork in Sync
Upstream moves while you work. Before starting any new branch, sync:
# Fetch all changes from upstream (doesn't modify your local branches)git fetch upstream
# Merge upstream/main into your local maingit switch maingit merge upstream/main
# Push the updated main to your forkgit push origin mainOr in one step using the GitHub CLI:
gh repo sync YOUR-USERNAME/git-github-security-learning --source fischer3-net/git-github-security-learningWhat happens when upstream changes while your branch is open? Your PR shows as “X commits behind main.” You need to update your branch:
git switch feat/my-featuregit fetch upstreamgit rebase upstream/main # Replay your commits on top of latest maingit push origin feat/my-feature --force-with-lease--force-with-lease is safer than --force — it refuses to push if the
remote branch has commits you haven’t seen, preventing you from accidentally
overwriting someone else’s work.
Reading CONTRIBUTING.md as a New Contributor
CONTRIBUTING.md is the social contract of an open-source project. It tells
you what the maintainers want, how to communicate with them, and what will get
your PR accepted vs. closed. Ignoring it is the single most common reason
first PRs are closed without merging.
The five things to look for in any CONTRIBUTING.md:
- Prerequisites and setup — do you have everything installed?
- What’s in scope — what kinds of contributions are welcomed?
- Process before writing code — does the project require an issue before a PR?
- Branch and commit conventions — what names and formats does the project expect?
- PR checklist — what must be true before you request a review?
The GitHub CLI (gh)
The gh CLI is GitHub’s official command-line tool. It wraps the GitHub API
and lets you do everything the browser can do from your terminal: create issues,
open PRs, request reviews, check CI status, merge, and more.
# Authenticationgh auth login # Authenticate with GitHubgh auth status # Check current auth status
# Repositoriesgh repo clone owner/repo # Clone a repogh repo fork owner/repo --clone # Fork and clone in one stepgh repo sync # Sync fork with upstream
# Issuesgh issue list # List open issuesgh issue create # Create an issue interactivelygh issue view 42 # View issue #42gh issue close 42 # Close issue #42
# Pull Requestsgh pr list # List open PRsgh pr create # Open a PR interactivelygh pr view 17 # View PR #17gh pr checkout 17 # Check out a PR's branch locallygh pr review 17 --approve # Approve a PRgh pr merge 17 --squash # Merge PR #17 with squash
# CI / Workflowsgh run list # List recent workflow runsgh run watch # Watch the current run livegh workflow run ci.yml # Trigger a workflow manuallyContributor vs. Collaborator vs. Maintainer
These three roles have distinct meanings and access levels:
| Role | Access | How you get it |
|---|---|---|
| Contributor | No direct write access | Anyone — you fork and open PRs |
| Collaborator | Write access to branches | Maintainer invites you |
| Maintainer | Admin access | Repository owner grants it |
Most open-source contributions come from people with no formal repository access. The fork-and-PR workflow is specifically designed for this — it lets anyone propose changes without requiring the maintainer to vet contributors before they can start working.
Exercise
Part 1 — Read the Contributor Docs
Before writing a single line of code, read the project’s contributor documentation the way a new external contributor would.
-
Open
CONTRIBUTING.md:Terminal window code CONTRIBUTING.md -
Find and answer these questions as you read:
- What four things does the project say make the best contributions?
- What kinds of contributions is the project unlikely to accept?
- For a new Specialist Agent, what file must be included alongside the code? (Find the Agent Contribution Checklist section.)
- What should you do before writing a new module? (Hint: it’s not opening a PR.)
- How long do maintainers aim to take for PR reviews?
-
Open
CODE_OF_CONDUCT.md:Terminal window code CODE_OF_CONDUCT.md -
Find:
- The four groups the community is explicitly welcoming to
- How to report a Code of Conduct violation
- One example of acceptable behaviour that’s specific to this project (not just a generic CoC item)
-
Check whether the Calculate Agent issue from Module 04 already exists:
Terminal window gh issue list --label "feat,starter-project"If it exists, note the issue number. If not, you’ll create one in Part 2.
Part 2 — Open an Issue (CLI)
Per CONTRIBUTING.md, significant contributions need an issue before a PR.
The Calculate Agent qualifies — it’s a new agent, not a small fix.
-
Create the issue using the GitHub CLI:
Terminal window gh issue create \--title "feat: implement Calculate Agent for arithmetic operations" \--body "## BackgroundThe Calculate Agent has been registered in the Orchestrator's AGENT_REGISTRYsince Module 02 (port 8003), but the implementation has never been contributed.This issue tracks the full implementation following the contributor process.## What needs to be doneImplement the Calculate Agent as a Specialist Agent in the A2A system.## Acceptance Criteria- [ ] Agent runs on port 8003 (configurable via CALCULATE_AGENT_URL)- [ ] Accepts standard A2A request schema (task, input)- [ ] Parses and evaluates arithmetic expressions from the input field- [ ] Returns status: error with a descriptive message for invalid expressions- [ ] Uses a safe expression parser — never eval() on user input- [ ] Unit tests cover success, error, and edge cases- [ ] Both Python and Node.js variants implemented- [ ] Agent registered in .env.example" \--label "feat,starter-project" -
Confirm it was created:
Terminal window gh issue list --limit 5Note the issue number — you’ll reference it in your PR description.
-
Assign it to yourself:
Terminal window gh issue edit <issue-number> --add-assignee "@me"
Part 3 — Fork and Configure Remotes
In this exercise, you’ll simulate the full fork workflow even though you already have a clone of your fork from earlier modules. The goal is to practice the remote configuration explicitly.
-
Check your current remotes:
Terminal window git remote -vYou should see
originpointing at your fork. Ifupstreamisn’t configured yet, add it now:Terminal window git remote add upstream https://github.com/fischer3-net/git-github-security-learning.gitReplace
fischer3-netwith the actual organisation or username that owns the upstream repository. -
Fetch from upstream to make sure you’re current:
Terminal window git fetch upstreamgit switch maingit merge upstream/maingit push origin main -
Verify the full remote picture:
Terminal window git remote -vgit log --oneline --graph --all --decorate | head -20You should see
origin/mainandupstream/mainlabels in the log, showing that your local Git knows about both.
Part 4 — Implement the Calculate Agent
Now do the actual work. You’ll implement the Python variant of the Calculate Agent following the A2A conventions established in earlier modules.
-
Create the feature branch:
Terminal window git switch -c feat/calculate-agent -
Create the agent directory and file:
Terminal window mkdir -p agents/calculatecode agents/calculate/main.py -
Implement the agent. Use
ast.literal_evalfor safe expression parsing — nevereval()directly on user input:"""Calculate Agent — Arithmetic Specialist AgentEvaluates basic arithmetic expressions passed via the A2A protocol.Routes on task: "calculate".Security: Uses ast.literal_eval indirectly via a safe parser.Never passes user input directly to eval()."""import astimport operatorimport osimport loggingfrom fastapi import FastAPIimport uvicorn# Add the project root to the path for shared modelsimport syssys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))from models import AgentRequest, AgentResponselogging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)app = FastAPI(title="Calculate Agent", version="1.0.0")# Allowed operators — whitelist approach prevents injectionSAFE_OPERATORS = {ast.Add: operator.add,ast.Sub: operator.sub,ast.Mult: operator.mul,ast.Div: operator.truediv,ast.Pow: operator.pow,ast.USub: operator.neg,}def safe_eval(expression: str) -> float:"""Safely evaluate an arithmetic expression without using eval().Parses the expression into an AST and walks it, only allowingnumeric literals and the operators in SAFE_OPERATORS.Raises ValueError for anything else."""def _eval(node: ast.AST) -> float:if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):return node.valueelif isinstance(node, ast.BinOp) and type(node.op) in SAFE_OPERATORS:left = _eval(node.left)right = _eval(node.right)if isinstance(node.op, ast.Div) and right == 0:raise ValueError("Division by zero")return SAFE_OPERATORS[type(node.op)](left, right)elif isinstance(node, ast.UnaryOp) and type(node.op) in SAFE_OPERATORS:return SAFE_OPERATORS[type(node.op)](_eval(node.operand))else:raise ValueError(f"Unsupported expression: {ast.dump(node)}")try:tree = ast.parse(expression.strip(), mode='eval')return _eval(tree.body)except (SyntaxError, ValueError) as e:raise ValueError(str(e))@app.get("/health")async def health():return {"agent": "calculate", "status": "healthy"}@app.post("/run")async def run(request: AgentRequest) -> AgentResponse:logger.info(f"Calculate Agent received: task={request.task}, input={request.input!r}")try:result = safe_eval(request.input)# Format cleanly: int if whole number, float otherwiseformatted = int(result) if result == int(result) else round(result, 8)return AgentResponse.success(agent="calculate",output=str(formatted),request_id=request.request_id,)except ValueError as e:logger.warning(f"Calculate Agent error: {e}")return AgentResponse.error(agent="calculate",error=f"Could not evaluate expression: {e}",request_id=request.request_id,)if __name__ == "__main__":port = int(os.getenv("CALCULATE_AGENT_PORT", "8003"))logger.info(f"Starting Calculate Agent on port {port}")uvicorn.run(app, host="0.0.0.0", port=port) -
Write a
README.mdfor the agent:Terminal window code agents/calculate/README.md# Calculate AgentEvaluates arithmetic expressions routed from the Orchestrator.## UsageTask keyword: `calculate````bashcurl -X POST http://localhost:8003/run \-H "Content-Type: application/json" \-d '{"task": "calculate", "input": "12 * 7"}'Response:
{"agent": "calculate", "status": "success", "output": "84"}Supported Operations
Addition (
+), subtraction (-), multiplication (*), division (/), exponentiation (**), and parentheses.Security
Expressions are parsed using Python’s
astmodule — nevereval(). Only numeric literals and whitelisted operators are permitted. -
Update
.env.examplewith the new port variable:Terminal window echo "CALCULATE_AGENT_PORT=8003" >> .env.exampleecho "CALCULATE_AGENT_URL=http://localhost:8003" >> .env.example -
Make three focused commits — one per logical change:
Terminal window git add agents/calculate/main.pygit commit -m "feat(calculate-agent): implement safe arithmetic expression evaluatorUses ast.parse + a recursive AST walker to evaluate arithmeticexpressions without ever calling eval() on user input.Whitelisted operators: +, -, *, /, **, unary minus.Returns status:error for invalid expressions and division by zero."git add agents/calculate/README.mdgit commit -m "docs(calculate-agent): add agent README with usage examples"git add .env.examplegit commit -m "chore(config): add CALCULATE_AGENT_PORT and URL to env example"
Part 5 — Test Before You Push
CONTRIBUTING.md requires that new agent contributions are tested.
Don’t open the PR until you’ve verified this works.
-
Start the Calculate Agent in one terminal:
Terminal window cd starter-project/pythonpython agents/calculate/main.py -
In a second terminal, run through the acceptance criteria:
Terminal window # Basic arithmeticcurl -s -X POST http://localhost:8003/run \-H "Content-Type: application/json" \-d '{"task": "calculate", "input": "12 * 7"}' | python -m json.tool# Expected: {"agent": "calculate", "status": "success", "output": "84"}# Floating pointcurl -s -X POST http://localhost:8003/run \-H "Content-Type: application/json" \-d '{"task": "calculate", "input": "10 / 3"}' | python -m json.tool# Division by zero — should return error, not crashcurl -s -X POST http://localhost:8003/run \-H "Content-Type: application/json" \-d '{"task": "calculate", "input": "5 / 0"}' | python -m json.tool# Injection attempt — should return error, not executecurl -s -X POST http://localhost:8003/run \-H "Content-Type: application/json" \-d '{"task": "calculate", "input": "__import__(\"os\").system(\"whoami\")"}' | python -m json.tool -
Verify the last test returns
status: "error"— not a system username. That’s the safe parser working correctly. -
Test routing through the Orchestrator:
Terminal window # Start Orchestrator (separate terminal)python orchestrator/main.pycurl -s -X POST http://localhost:8000/run \-H "Content-Type: application/json" \-d '{"task": "calculate", "input": "2 ** 10"}' | python -m json.tool# Expected: {"agent": "calculate", "status": "success", "output": "1024"}
Part 6 — Open the PR with the GitHub CLI
-
Push the branch to your fork:
Terminal window git push origin feat/calculate-agent -
Open the PR entirely from the terminal using
gh:Terminal window gh pr create \--title "feat(calculate-agent): implement safe arithmetic expression evaluator" \--body "## What does this PR do?Implements the Calculate Agent as a new Specialist Agent in the A2A system.The Orchestrator already had \`calculate\` registered in its routing table(added in Module 02) — this PR provides the implementation.The agent evaluates arithmetic expressions using Python's \`ast\` modulewith a recursive AST walker. It never calls \`eval()\` on user input.## Related IssueCloses #$(gh issue list --search 'Calculate Agent' --json number --jq '.[0].number')## Type of Change- [x] 🤖 Starter project — changes to \`starter-project/python/\`## Module(s) AffectedStarter Project — Python variant## Testing\`\`\`bashpython agents/calculate/main.pycurl -X POST http://localhost:8003/run \\-H 'Content-Type: application/json' \\-d '{\"task\": \"calculate\", \"input\": \"12 * 7\"}'# Expected: {\"agent\": \"calculate\", \"status\": \"success\", \"output\": \"84\"}\`\`\`## Starter Project Checklist- [x] No secrets or API keys hardcoded- [x] CALCULATE_AGENT_PORT added to .env.example- [x] Agent is already registered in Orchestrator's routing table- [x] Expression evaluator tested against injection attempts- [ ] Node.js variant — tracked as a follow-up issue" \--assignee "@me" -
View the PR you just created:
Terminal window gh pr view --webThis opens it in your browser. Confirm the description rendered correctly — especially the checklist items and the issue link.
-
Check CI status from the terminal:
Terminal window gh pr checksThis shows the status of every required check on the PR — the same information visible in the PR’s Checks tab.
Part 7 — Simulate Fork Drift and Sync
While your PR is open, upstream moves. Simulate this and practice the sync workflow.
-
Switch to
mainlocally:Terminal window git switch main -
Simulate an upstream commit by making a direct change to
main(representing what another contributor merged upstream while you were working on your branch):Terminal window echo "# Calculate Agent tracked in Issue #$(gh issue list --search 'Calculate' --json number --jq '.[0].number')" >> orchestrator/README.mdgit add orchestrator/README.mdgit commit -m "docs(orchestrator): note Calculate Agent issue reference"git push origin main -
Now your feature branch is behind
main. Check:Terminal window git switch feat/calculate-agentgit log --oneline --graph --all | head -15You’ll see
mainhas moved ahead of where your branch forked. -
Rebase your branch onto the current
main:Terminal window git fetch upstream 2>/dev/null || git fetch origingit rebase origin/mainSince your changes don’t overlap, this rebase will be clean.
-
Push the rebased branch — you need
--force-with-leasebecause the rebase rewrote the commit SHAs:Terminal window git push origin feat/calculate-agent --force-with-lease -
Return to the PR on GitHub:
Terminal window gh pr view --webThe PR now shows the rebased commits and is up to date with
main. The “X commits behind main” warning is gone.
Part 8 — Review Another Contributor’s PR
Healthy open-source projects need reviewers as much as contributors. Practice the review side of the workflow.
-
List open PRs in the repository:
Terminal window gh pr list -
If another PR exists, check it out locally:
Terminal window gh pr checkout <pr-number>This fetches the PR’s branch and switches to it — you can run the code locally without manually adding someone else’s fork as a remote.
-
Test the code, then leave a structured review using the CLI:
Terminal window # Approve with a commentgh pr review <pr-number> \--approve \--body "Tested locally — the agent handles edge cases correctly.One suggestion: add a comment above safe_eval explaining whyast.parse is used instead of eval(). Future contributors maybe tempted to simplify it."# Or request changesgh pr review <pr-number> \--request-changes \--body "The division by zero case returns a 500 rather than astructured error response. The agent should catch this andreturn AgentResponse.error() to match the A2A contract." -
Switch back to your own branch when done:
Terminal window git switch feat/calculate-agent
The gh CLI Reference
# Fork a repo and clone it in one stepgh repo fork owner/repo --clone --remote
# Sync your fork with upstreamgh repo sync # syncs current repogh repo sync owner/fork --source owner/upstream
# View repo infogh repo viewgh repo view owner/repo --web # open in browser
# Clone any repogh repo clone owner/repo# List and filtergh issue listgh issue list --label "feat" --assignee "@me"gh issue list --search "Calculate Agent"
# Creategh issue create --title "..." --body "..." --label "feat"
# Managegh issue view 42gh issue edit 42 --add-label "in-progress"gh issue close 42 --comment "Fixed in PR #17"gh issue reopen 42# Creategh pr create # interactivegh pr create --title "..." --body "..." --draft
# Review and managegh pr listgh pr view 17gh pr checkout 17 # check out locallygh pr checks # CI statusgh pr review 17 --approvegh pr review 17 --request-changes --body "..."gh pr merge 17 --squash --delete-branch
# Editgh pr edit 17 --add-label "needs-review"gh pr ready # mark draft as ready# List and triggergh workflow listgh workflow run ci.yml --ref maingh workflow run ci.yml --field target_agent=calculate
# Monitorgh run list --workflow=ci.yml --limit 5gh run view # most recent rungh run watch # live streamgh run view --log-failed # only failures
# Artifactsgh run download <run-id>What Makes a Good Upstream PR
When contributing to a project you don’t own, the bar for your PR is slightly higher than for your own repository — you’re asking someone to take responsibility for your code.
Before opening a PR to upstream:
✅ Read CONTRIBUTING.md front to back✅ The issue exists and links to your PR✅ Your branch is current with main (no drift)✅ CI is passing on your branch✅ CODEOWNERS will request the right reviewers automatically✅ The checklist in the PR template is complete✅ You've tested the acceptance criteria from the issue
After opening:
✅ Respond to review comments within a reasonable time✅ Push fixes as new commits on the same branch (don't close and re-open the PR)✅ Mark conversations as resolved after addressing them✅ Don't merge without at least one approval
What to avoid:
❌ Opening a PR before the work is ready (use Draft instead)❌ Combining multiple unrelated changes in one PR❌ Rewriting your entire commit history after review starts❌ Arguing against every review comment — some are judgment calls❌ Pinging reviewers more than once per weekSummary
In this module you:
- Understood the two-remote model —
origin(your fork) andupstream(the source repository) — and why it exists - Read
CONTRIBUTING.mdandCODE_OF_CONDUCT.mdas an incoming contributor, answering the questions that determine whether a PR gets merged - Used the GitHub CLI (
gh) to create an issue, open a PR, check CI status, and leave a review — the entire workflow without leaving the terminal - Implemented the Calculate Agent using a safe AST-based expression evaluator, following the A2A agent contract and the project’s security practices
- Simulated fork drift — upstream moving while your branch was open —
and resolved it with
git rebaseand--force-with-lease - Practised the review side of open-source contribution by checking out
and testing another PR locally via
gh pr checkout
The contributor workflow you practiced here is what every external pull request to this project follows. Understanding both sides — contributor and maintainer — makes you a better collaborator regardless of which role you’re in on any given day.
What’s Next
Module 08 · Packages, Releases & GitHub Pages →
You’ll tag the first versioned release of the A2A project, generate auto-release notes from merged PRs, publish the Orchestrator as a Docker image to GitHub Packages, and deploy the course documentation to GitHub Pages — the full production distribution pipeline for an open-source AI project.