Skip to content

Commit c6df306

Browse files
committed
changing format back
1 parent 016e64d commit c6df306

5 files changed

Lines changed: 59 additions & 51 deletions

File tree

instruqt/01-hello-tailnet/assignment.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,24 @@ timelimit: 900
4444
enhanced_loading: null
4545
---
4646

47+
# Exercise 1: Hello `tailnet`
48+
4749
Your first Workflow on the shared Temporal server, accessed through the Tailscale network.
4850

49-
# Background
51+
## Background
5052

5153
A Temporal dev server is running on a remote VPS, exposed to this Tailscale network via [temporal-ts-net](https://github.com/temporal-community/temporal-ts-net). **Once you join the tailnet in Step 1**, you'll be able to reach it at `temporal-dev:7233` (gRPC) and via the **Temporal UI** tab. (The UI tab may show a connection error until Step 1 completes; that's expected.)
5254

5355
The Workflow you'll run gets your exercise environment's public IP address, then geolocates it. This exercise is the foundation the rest of the workshop builds on: every later exercise assumes Workers and the Temporal Server can reach each other over the `tailnet` rather than the public internet.
5456

55-
# Environment
57+
## Environment
5658

5759
All code for this exercise lives in `exercises/01_hello_tailnet/`. Inside that directory:
5860

5961
- **`practice/`** is where you do your work. Each file has one or more **TODO** comments pointing at the change you need to make.
6062
- **`solution/`** contains the finished version of every file. If you get stuck or want to double-check your work, compare against the matching file in `solution/`. Don't run from `solution/`, run from `practice/`.
6163

62-
# Step 1: Join the `tailnet`
64+
## Step 1: Join the `tailnet`
6365

6466
Your Exercise Environment has the Tailscale client and an auth key available as `$TS_AUTHKEY`.
6567

@@ -77,17 +79,17 @@ tailscale status
7779

7880
You should see all of the devices that are on the tailnet, including the Temporal Server `temporal-dev` in the list.
7981

80-
# Step 2: Verify the Temporal Config File
82+
## Step 2: Verify the Temporal Config File
8183

8284
Temporal CLI and SDKs support configuring a Temporal Client using environment variables and TOML configuration files, rather than setting connection options programmatically in your code. This decouples connection settings from application logic, making it easier to manage different environments such as development, staging, and production without code changes.
8385

8486
This has already been set up for you in this environment. To verify, open `temporal.toml` in the [button label="Code Editor" background="#444CE7"](tab-0) tab. It's already in place in the workshop directory and the SDK is pointed at it via `TEMPORAL_CONFIG_FILE`. You should see two profiles: `default` (localhost) and `tailnet` (pointing at `temporal-dev:7233`).
8587

86-
# Step 3: Configure Your Application to Connect to the `tailnet` Profile
88+
## Step 3: Configure Your Application to Connect to the `tailnet` Profile
8789

8890
Both `worker.py` and `starter.py` currently load the `default` profile, which points at localhost. You need to point them at the `tailnet` profile so they connect to the shared Temporal server. This is a one-line change in each file.
8991

90-
## 3a. Edit `exercises/01_hello_tailnet/practice/worker.py`
92+
### 3a. Edit `exercises/01_hello_tailnet/practice/worker.py`
9193

9294
Find the **TODO** in `worker.py` and pass `profile="tailnet"` to `load_client_connect_config`.
9395

@@ -104,7 +106,7 @@ config = ClientConfig.load_client_connect_config()
104106
config = ClientConfig.load_client_connect_config(profile="tailnet")
105107
```
106108

107-
## 3b. Edit `exercises/01_hello_tailnet/practice/starter.py`
109+
### 3b. Edit `exercises/01_hello_tailnet/practice/starter.py`
108110

109111
The same change in `starter.py`:
110112

@@ -123,7 +125,7 @@ config = ClientConfig.load_client_connect_config(profile="tailnet")
123125

124126
Now both the worker and starter read the `tailnet` profile from `temporal.toml` and connect to the shared Temporal server.
125127

126-
# Step 4: Add your name to the Workflow ID
128+
## Step 4: Add your name to the Workflow ID
127129

128130
Workflow IDs must be unique on a Temporal Server, and prefixing yours with your name lets you find your run among everyone else's in the shared Temporal UI. `USER_ID` is already set from the sign-up form as `WORKSHOP_USER_ID` and wired into `starter.py`.
129131

@@ -141,7 +143,7 @@ id=f"geo-ip-{uuid.uuid4()}",
141143
id=f"{USER_ID}-geo-ip-{uuid.uuid4()}",
142144
```
143145

144-
# Step 5: Start the Worker
146+
## Step 5: Start the Worker
145147

146148
Now you are ready to run your Workflow. First, start the Worker. This is the process that will execute your Temporal application.
147149

@@ -156,7 +158,7 @@ You should see: `INFO:root:Connecting to Temporal at temporal-dev:7233`
156158

157159
Once it has connected you will see output similar to: `INFO:root:Starting worker on task queue: your-name-hello-tailnet`
158160

159-
# Step 6: Run the Workflow
161+
## Step 6: Run the Workflow
160162

161163
Once the Worker has started you are ready to run your Workflow.
162164

@@ -176,7 +178,7 @@ Your IP address: 257.257.257.257
176178
Your location: Alderaan, Core Worlds
177179
```
178180

179-
# Step 7: Check the Temporal UI
181+
## Step 7: Check the Temporal UI
180182

181183
Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find your Workflows by searching for your user ID. You should see your Workflow, along with all other attendees in the workshop!
182184

@@ -186,7 +188,7 @@ Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find
186188

187189
Your Worker is running in your exercise environment hosted in a GCP region. The Temporal Server is running on a VPS in DigitalOcean's San Francisco region. Tailscale provided secure access for the Worker to communicate with the server without exposing the server to the public internet.
188190

189-
# Step 8: Route egress through an exit node
191+
## Step 8: Route egress through an exit node
190192

191193
Tailscale also allows for setting an exit node on your `tailnet`, acting as a full tunnel VPN.
192194

@@ -212,7 +214,7 @@ Unset your exit node before moving on:
212214
tailscale set --exit-node=
213215
```
214216

215-
# Wrapping Up
217+
## Wrapping Up
216218

217219
In this exercise you:
218220

instruqt/02-explore-tailscale/assignment.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ timelimit: 1200
3939
enhanced_loading: null
4040
---
4141

42+
# Exercise 2: Exploring Your Tailscale Network
43+
4244
Now that you've run a Workflow through the `tailnet`, let's look at the network itself. You'll discover what's on the `tailnet`, see how Tailscale identity works, and run a Go Worker that joins the `tailnet` as its own node via [`tsnet`](https://pkg.go.dev/tailscale.com/tsnet) instead of riding on the Exercise Environment's system client.
4345

44-
# Background
46+
## Background
4547

4648
In Exercise 1 your Python Worker reached Temporal through the Exercise Environment's Tailscale client. The environment was on the `tailnet`, and the Worker inherited that connectivity. `tsnet` is the library inside Tailscale that lets a process join the `tailnet` directly, as its own node, with no system-wide install. Every Tailscale binary uses it under the hood, and you can embed it in any Go program. That's what `temporal-ts-net` does to put the Temporal dev server on the `tailnet`, and it's the pattern you'll use in Exercise 4 for the metrics watcher.
4749

48-
# Environment
50+
## Environment
4951

5052
Steps 1 through 3 poke at the `tailnet` from the **Worker** terminal and don't touch code. Step 4 takes the system Tailscale client offline so the later `tsnet` work has to stand on its own. Starting in Step 5, the Go code for this exercise lives under `go-hello-tsnet/` in the **Code Editor** tab. Inside that directory:
5153

@@ -66,7 +68,7 @@ Go and the `tsnet` library are already installed, and the module cache has been
6668
> tailscale up --auth-key="$TS_AUTHKEY" --hostname="${WORKSHOP_USER_ID}-env"
6769
> ```
6870
69-
# Step 1: See what's on the `tailnet`
71+
## Step 1: See what's on the `tailnet`
7072
7173
Before you add another Worker, it's worth looking at who else is already on the `tailnet`. `tailscale status` lists every node your machine can see, along with its IP, hostname, and connection path.
7274
@@ -82,7 +84,7 @@ You should see:
8284
- **`temporal-dev`**, the VPS running the shared Temporal dev server
8385
- **Other attendee machines**, everyone else in the workshop, each with their own hostname
8486

85-
# Step 2: Ping the Temporal Server
87+
## Step 2: Ping the Temporal Server
8688

8789
`tailscale status` tells you what's on the `tailnet`. `tailscale ping` tells you *how* you reach any given node, whether the first packets go through a Tailscale relay (DERP) or straight over a direct encrypted WireGuard path.
8890

@@ -100,7 +102,7 @@ pong from temporal-dev (100.109.42.22) via 167.71.156.227:35753 in 40ms
100102

101103
The first line may say `pong from temporal-dev (100.109.42.22) via DERP(...)`, meaning it was relayed through Tailscale's infrastructure. Subsequent lines (like the sample above) report `via <public-IP>:<port>`, a direct encrypted WireGuard path with no relay. Once the direct path is established, every packet to `temporal-dev` flows over it.
102104

103-
# Step 3: Check your Tailscale identity
105+
## Step 3: Check your Tailscale identity
104106

105107
Every node on the `tailnet` has an identity: a machine name, tags, and the user that owns the node. Services on the `tailnet` use this identity to authorize or rate-limit you without any API keys on your side. In Exercise 3 you'll see Aperture use exactly this mechanism to attach your name to every LLM call.
106108

@@ -112,7 +114,7 @@ tailscale whois $(tailscale ip -4)
112114

113115
You should see the machine name, tags, and the Tailscale user associated with this node.
114116

115-
# Step 4: Take the system Tailscale client offline
117+
## Step 4: Take the system Tailscale client offline
116118

117119
Steps 1 through 3 leaned on the system `tailscale` binary to explore the `tailnet`. The Go Worker you're about to build joins the `tailnet` on its own via `tsnet`, so it doesn't need the binary at all. Prove that by taking it down now, before the Worker ever runs.
118120

@@ -130,7 +132,7 @@ tailscale status
130132

131133
You should see `Stopped` (or `Logged out`) instead of the `tailnet` nodes from Step 1. Your Exercise Environment is no longer on the `tailnet`. The Go Worker is about to put itself there on its own.
132134

133-
# Step 5: Configure the `tsnet.Server`
135+
## Step 5: Configure the `tsnet.Server`
134136

135137
The `tsnet.Server` struct is how a Go program declares that it wants to be a `tailnet` node. You give it a hostname, a state directory, and an auth key, and once you call `Start()` your process has its own `tailnet` IP.
136138

@@ -152,7 +154,7 @@ tsNode := &tsnet.Server{
152154

153155
The rest of the function (`tsNode.Start()`, `tsNode.Up(upCtx)`, the log line) is already in place.
154156

155-
# Step 6: Dial Temporal through `tsnet`
157+
## Step 6: Dial Temporal through `tsnet`
156158

157159
The `tsnet.Server` you just configured gives your Worker its own node on the `tailnet`. Now you need to tell the Temporal Go SDK to use it, so the gRPC connection to `temporal-dev:7233` flows through `tsnet` instead of the system network stack.
158160

@@ -166,7 +168,7 @@ grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error)
166168

167169
Every byte the SDK sends now flows through `tsNode.Dial`, which routes over the `tailnet`.
168170

169-
# Step 7: Start the Go Worker
171+
## Step 7: Start the Go Worker
170172

171173
With both TODOs filled in, the Worker is ready to join the `tailnet` and connect to Temporal on its own.
172174

@@ -185,7 +187,7 @@ connected to temporal at temporal-dev:7233 via tsnet
185187
Starting Go worker on task queue: YOUR-USER-ID-hello-tsnet
186188
```
187189

188-
# Step 8: Confirm the Go Worker is on the `tailnet`
190+
## Step 8: Confirm the Go Worker is on the `tailnet`
189191

190192
The Go Worker should now appear as its own node on the `tailnet`, even with the system Tailscale client still offline from Step 4. To verify from the command line, bring the system client back up and check `tailscale status`.
191193

@@ -198,7 +200,7 @@ tailscale status | grep -- '-ex2-go-worker'
198200

199201
You should see a new row, `<your-user-id>-ex2-go-worker-<suffix>`, separate from the Exercise Environment itself. The Worker joined the `tailnet` on its own via `tsnet` while the system client was offline. That's the whole point of Step 4.
200202

201-
# Step 9: Run the Workflow
203+
## Step 9: Run the Workflow
202204

203205
Now trigger the same geo-IP Workflow from Exercise 1. This time the activities execute on the Go `tsnet` Worker you just started.
204206

@@ -211,7 +213,7 @@ go run . starter
211213

212214
You should see your public IP address and location printed, same as Exercise 1, but this time the Worker that executed the activities was the Go `tsnet` Worker, not the Python Worker from Exercise 1.
213215

214-
# Step 10: Check the Temporal UI
216+
## Step 10: Check the Temporal UI
215217

216218
Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find your `<your-user-id>-hello-tsnet` Workflow. Click into it and look at the worker info on each activity. The task queue is `<your-user-id>-hello-tsnet`, and the worker identity reflects the Go process rather than the Python one from Exercise 1.
217219

@@ -221,7 +223,7 @@ Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find
221223

222224
Same Temporal Server, same Workflow, different Worker transport. The Python Worker in Exercise 1 relied on the Exercise Environment's Tailscale client to reach Temporal. The Go Worker you just ran carries its own `tsnet` node inside the process itself, joins the `tailnet` on startup, and dials Temporal through that embedded node.
223225

224-
# Wrapping Up
226+
## Wrapping Up
225227

226228
In this exercise you:
227229

instruqt/03-weather-agent/assignment.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ timelimit: 1500
4444
enhanced_loading: null
4545
---
4646

47+
# Exercise 3: Python Weather Agent
48+
4749
Build a durable AI agent that chains tool calls, with all LLM requests secured through Aperture on the `tailnet`.
4850

49-
# Background
51+
## Background
5052

5153
This exercise has two phases.
5254

@@ -61,7 +63,7 @@ This exercise has two phases.
6163

6264
Every LLM call goes through **Aperture** instead of directly to OpenAI. Aperture holds the shared API key, identifies you by your Tailscale identity, and enforces rate limits. The same pattern applies in Exercise 4 with Anthropic's Claude.
6365

64-
# Environment
66+
## Environment
6567

6668
All code for this exercise lives in `exercises/03_weather_agent/`. Inside that directory:
6769

@@ -80,7 +82,7 @@ All code for this exercise lives in `exercises/03_weather_agent/`. Inside that d
8082
> tailscale up --auth-key="$TS_AUTHKEY" --hostname="${WORKSHOP_USER_ID}-env"
8183
> ```
8284
83-
# Step 1: Route LLM calls through Aperture
85+
## Step 1: Route LLM calls through Aperture
8486
8587
This step begins **Phase A: Tool-Calling**. The change is small but load-bearing. Instead of pointing the OpenAI client at `api.openai.com`, you point it at Aperture, the `tailnet`-only gateway that attaches your Tailscale identity, enforces rate limits, and swaps in the real API key server-side.
8688
@@ -98,7 +100,7 @@ client = AsyncOpenAI(
98100
99101
`APERTURE_URL` is already exported in your environment. The OpenAI client will now send requests to Aperture, which forwards them to OpenAI with the shared API key after attaching your Tailscale identity.
100102

101-
# Step 2: Start the Phase A Worker
103+
## Step 2: Start the Phase A Worker
102104

103105
Now start the Worker. This is the process that executes the Workflow and its tool activities.
104106

@@ -111,7 +113,7 @@ uv run worker.py
111113

112114
You should see the Worker connect to Temporal and start listening on its task queue.
113115

114-
# Step 3: Run the Phase A Workflow
116+
## Step 3: Run the Phase A Workflow
115117

116118
With the Worker running, trigger the Workflow from the [button label="Starter" background="#444CE7"](tab-2) terminal.
117119

@@ -128,7 +130,7 @@ You should see the LLM call the weather tool and return results. Click the [butt
128130

129131
Your Worker executed a Workflow where the LLM chose to call a tool instead of answering directly. The LLM call itself flowed through Aperture, which authenticated you by your Tailscale identity and forwarded the request to OpenAI with the shared API key. Your code never touched an OpenAI API key.
130132

131-
# Step 4: Enable the agentic loop
133+
## Step 4: Enable the agentic loop
132134

133135
This step begins **Phase B: Agentic Loop**. The difference from Phase A is that the Workflow now keeps handing the LLM tool results until the LLM decides it has enough information to answer, instead of stopping after one tool call.
134136

@@ -140,7 +142,7 @@ while True:
140142

141143
This turns the single-shot tool call from Phase A into a loop that repeatedly calls the LLM, executes whichever tool the LLM picked, feeds the result back, and stops only when the LLM decides it's done.
142144

143-
# Step 5: Execute the chosen activity dynamically
145+
## Step 5: Execute the chosen activity dynamically
144146

145147
Still in `agent_workflow.py`, find **TODO 3** and replace the empty string with `item.name`:
146148

@@ -154,7 +156,7 @@ tool_result = await workflow.execute_activity(
154156

155157
`item.name` is whichever tool the LLM picked on this iteration, such as `get_ip_address`, `get_location_info`, or `get_weather_alerts`. Temporal runs it as a dynamic activity, so the Worker does not hard-code which tool to call; Temporal dispatches by name.
156158

157-
# Step 6: Restart the Worker as the agent Worker
159+
## Step 6: Restart the Worker as the agent Worker
158160

159161
The Phase A Worker registered the single-shot tool-calling Workflow. For Phase B you need the agent Workflow registered, which means restarting the Worker with the `--agent` flag.
160162

@@ -166,7 +168,7 @@ uv run worker.py --agent
166168

167169
You should see the Worker reconnect to Temporal and start listening on its task queue, this time with the agent Workflow registered.
168170

169-
# Step 7: Run the agentic Workflow
171+
## Step 7: Run the agentic Workflow
170172

171173
Now ask the agent a question that requires multiple tool calls to answer.
172174

@@ -182,7 +184,7 @@ Watch the Worker logs. The LLM chains through multiple tools before responding:
182184

183185
The LLM made autonomous decisions about which tool to call next, and Temporal recorded every call, input, and output in the Workflow history. If the process had crashed halfway through, Temporal could replay the history on a new Worker and the agent would resume from exactly where it left off, even partway through a multi-tool reasoning chain.
184186

185-
# Step 8: Explore the Aperture UI
187+
## Step 8: Explore the Aperture UI
186188

187189
Open the [button label="Aperture UI" background="#444CE7"](tab-4) tab to see every LLM call your Workers made.
188190

@@ -196,7 +198,7 @@ Click the **Adoption** tab for a cost and token-usage breakdown across models an
196198

197199
> **Note:** Every Instruqt machine authenticated using the same `tag:infra`, so the Dashboard and Logs show requests from all attendees, not just yours. In a real deployment, Aperture attributes usage per user via their Tailscale identity from your IDP. Agentic workloads should have their own tags too -- both so Aperture tracks them separately from human users and because zero-trust ACLs depend on a well-defined tag taxonomy to enforce least-privilege access. The workshop `tailnet` has fully open ACLs for simplicity; in production you would give each user and each agent only the access they need.
198200
199-
# Wrapping Up
201+
## Wrapping Up
200202

201203
In this exercise you:
202204

0 commit comments

Comments
 (0)