Decisions
Pinned so they don’t get re-litigated. Each links to the doc that argues it. Reversing one requires editing this file in the same PR.
- The write path is always the PR. No write GUI, ever. Browsing goes to behold over the graph. (plan , positioning )
- No new format. Declared source in, native artifacts out, so the estate outlives the toolchain. The anti-IAMbic clause. On Terraform the native artifact is HCL the provider applies directly, and the state file is bookkeeping, never the system of record (decision 32). (landscape )
- Manages what it declares, audits what it owns. Not a CSPM, not a CIEM. Estate-wide scanning belongs to an auditor like Prowler. Chat intake is not a JIT catalog. A JIT product grants on approval, water park’s concierge produces a diff a human merges (decision 18). (positioning , landscape )
- AWS is the wedge; cross-cloud is act two. Track B is needs-design until persona equivalence is solved. (plan )
- Humans get permission sets, workloads get roles. No IAM users, no IAM groups. (landscape , design/personas )
- Merge-then-apply is the default. Apply-from-PR is a later option behind the freshness digest. (pr-automation )
- PR automation is compiled, not served. A compile target of the CI generators; a standing runner is deferred to requirements capture (C5). (pr-automation )
- Break-glass expiry is cloud-side. Temporal being down can delay cleanup, never extend access. TEAM interop documented, not replaced. (design/break-glass )
- Guardrails roll out warn-minor / error-major with ratchet baselines. An upgrade cannot break a satellite without a warn cycle. (design/guardrail-rollout )
- The shared module is the delegation contract. The guardrail
checks, the boundary ARN and the
workload_rolemodule live in one versioned module a satellite consumes from its first file. Built in lesson I8 (amended under decision 26, it ships with the lesson, not “from the start” of a kit). (plan , IAM, lesson 8 ) - Account vending is out of scope. water park references accounts (registry, reference-existing); Control Tower / org-formation vend them. (design/multi-account )
- The apply credential is bounded. water park must not be able to escalate water park; the apply role carries its own permission boundary. (threat-model )
- Federation trust is estate; the issuer is not. water park declares OIDC/SPIFFE/Roles-Anywhere trust anchors as code with the strictest lint and drift severity, and never operates an identity issuer. (design/workload-identity )
- Agents propose; they never approve, apply, or signal. The agent is an untrusted author whose PRs are verified identically to human PRs. Trust attaches to the compiled checks, never to the author. (design/agentic )
- The sandbox is never a principal. An untrusted agent sandbox holds no cloud credentials and is never a federation subject. Identity attaches to the verb service outside the sandbox boundary; the sandbox receives only a conversation-scoped verb-API token. (design/agentic , design/workload-identity )
- water park owns the AWS governance reconcile, or nobody does.
The prior art here is chant’s archived
aws-warden, which covered the OU tree, SCPs, Identity Center, the org trail and a protected break-glass. It is adopted on water park’s terms, which means declared source instead of a YAML tree and the PR as the write path, with the cycle design and guardrail set taken verbatim. (upstream.md ) - The requester’s identity is declared, never asserted. A chat or intake identity earns standing only by appearing in a principal’s leaf file under the repo’s own review. An unmapped identity gets a refusal carrying the enrollment path, never a PR. Until the intake runtime attests the requesting author, the requester is rendered on the PR as an unverified claim, in those words. (design/agentic )
- Chat is intake and notification; it is never an approval surface. No approve button in a channel, no gate a message can satisfy, no Op a reply can signal. Decision 14’s corollary, pinned separately because a chat front-end is exactly where someone will later propose one. (design/agentic )
- One estate: AWS IAM, plus the code host’s own protection. Application-level authorization is out of scope. The code-host resources that guard the repo — branch protection, CODEOWNERS — are declared and drift-watched because merge rights are grant rights (principle 6, threat-model boundary 1); that is the repo protecting itself, not a second estate. Cross-cloud legs (Track B) stay parked. (plan , threat-model )
- Satellites create roles; the boundary is what makes that safe. A
satellite declares its own workload roles inside a permission boundary
the central repo owns, enforced by lint at build and by the
iam:PermissionsBoundarycondition at apply. Central keeps personas, the boundary, and the guardrails. (design/delegation ) - CODEOWNERS is generated, not authored. The routing is derived from the principal files it routes, emitted, and drift-watched. Rerouting review of a team’s access is a visible diff, never a quiet dotfile edit. (threat-model )
- A live proof never runs in a job an untrusted author can trigger. PR-time validation is credential-free — Floci plus the full lint pack. Access Analyzer proofs run post-merge-queue or behind a maintainer-applied label. Applies to fork PRs and agent-authored PRs identically. (threat-model )
- The pattern is backend-blind; the course picks one. water park is a way of holding access, not a tool. Terraform and OpenTofu are what the course teaches (decision 31), and chant is a typed backend the design docs describe. What makes them interchangeable is the change manifest, the plan reduced to a common shape, which is the review, evidence and access-review object everywhere. Everything backend-specific stays behind it. (plan , pr-automation )
- Approval binds to the manifest, and the PR is the envelope. A reviewer approves the rendered change manifest identified by its digest, and apply refuses when the replanned manifest or the live estate has moved. On Terraform the saved plan file is that object and the refusal is native, since a saved plan will not apply against state it no longer matches. The manifest is never the system of record, declared source in git is (decision 2). Extends decision 6. (pr-automation )
- The IAM kit is the IAM scenario’s backlog. Superseded in framing by decision 26: the kit is neither a product nor parked; tracks A–E are the source material for lessons I1–I15 and IA, mapped in issues.md . Nothing there is “parked” except Track B and the org-layer reconcile, which are appendix lessons. (plan , issues )
- water park is an IAM project repo that comes with courses; the courses are about the Accessible Ops properties. Two courses, each lesson naming the properties it demonstrates: course 1 is Fountain (the agent side, built up to the propose loop abstracted from Mend, Rounds and dns-desk); course 2 is the IAM repo, where the agent goes to work. The IAM scenario is the worked example because it exercises both vehicles and most of the properties; it need not cover every one. Each lesson is a card, an optional video, one activity; it runs self-paced or live. No onboarding metaphor; titles are plain. (plan , page model )
- Solo is Floci; live is real. The free path deploys to Floci and
says per lesson what Floci cannot show (Organizations, Identity
Center, Access Analyzer;
iam:PermissionsBoundaryenforcement unverified). A facilitated session uses real sandbox accounts, real zones, real repos, from checkpoints. (demo ) - Drift reconcile follows Rounds’ rules. One PR per owned resource on a derived branch with a marker in the body; the PR restores the declared state; an operator who wants the change kept edits the PR to declare it; closing unmerged is a no for that finding until relabeled; a capped number open at once; never a PR for a foreign resource; state lives in the code host. The same rules govern the watcher (I13). (IAM, lesson 7 , Rounds README)
- Containment claims need a hosted sandbox provider. The self-hosted runner is trusted mode: no isolation, no egress policy (Fountain ADR 0022). Any lesson that says “default-deny egress” or “no credentials can leave” runs on Sprites, E2B or Daytona; on a runner the lesson says so. (Fountain, lesson 10 )
- The verb service is a server with policy; Mend, Rounds and dns-desk are the reference implementations. Decision 15’s “verb service outside the sandbox” is Rounds’ server: the agent holds a read-only grant, the server holds the write credential for one target for one proposal, enforces the rules the prompt cannot, and renders the PR body from the same objects it reports. Where a human is present, Mend’s form (the PR opened from the human’s browser with the human’s token) is enough. No verb service is built ahead of need. (propose loop , Fountain, lesson 8 , Fountain, lesson 9 , design/agentic )
- The course runs on Fountain and Terraform. water park is a GitOps
pattern, not a toolchain. Any agent runtime and any declarative
applier can drive it, and naming the pair is a course decision rather
than a property of the pattern. This course pairs Fountain with
Terraform because both run free on a laptop and Terraform is where
most orgs already are. The repo is one resource per
.tffile and the directory is the module, so nothing assembles anything.terraform planis the plan,terraform applyin a gated job is the only write,plan -detailed-exitcodeis the drift watch,importblocks are adopt-in-place,terraform validateandtflintare the editor check, and Access Analyzervalidate-policyandcheck-no-new-accessare the proofs, since those are cloud APIs and not part of any toolchain. The agent app is the AWS desk , where direct mode is dns-desk’s posture and repo mode is the course’s. chant remains a backend the design docs describe (decision 23) and is not taught. Replacing either half is an edit to this decision. (aws-desk , plan ) - The state file is a cost the course names out loud. Accessible Ops
XI says the live system is the truth and warns about a tool that hosts
its own state. Terraform hosts one, and water park does not pretend
otherwise. The mitigations are that state lives in
waterpark-securitywith locking and is never the system of record (decision 2), that every read of the estate in these lessons goes to the cloud rather than to state, and that the drift watch compares declared against live. Lesson I4 teaches the cost and lesson I7 teaches the mitigation. A backend without a state file scores better on this property and the course says so rather than hiding it. (IAM, lesson 4 , IAM, lesson 7 ) - The access repo is this repo. The Terraform the IAM course builds,
which is the
envs/<env>/layout, the baseline module, the sharedworkload_rolemodule, the tflint rule pack,proofsandrender-delta, the apply workflow and the Floci local path, lands in this checkout besidecontent/andskills/rather than in a sibling repo. The estate already says the repo a student clones is the repo the course puts under management, and one clone, one PR flow and one CI keep that literal. The HCL root is a top-levelaccess/directory, so every path a lesson gives startsaccess/envs/<env>/. The cost is named out loud. The site repo carries a state backend config, an OIDC apply workflow and CODEOWNERS gating.tffiles, so course PRs and access PRs share one review queue. Lesson checkpoints are git tags here. (estate , aws-desk , plan ) - The desk edits directly, and there is no
scripts/request. In repo mode the desk locates the file by convention, makes the one edit itself, then runsterraform validate,tflint,proofs,terraform planandrender-delta, and opens the PR. The domain-verb idea in design/agentic keeps its read side, which isaccess/scripts/whocan,access/scripts/expiringandaccess/scripts/offboard --preview, and it keeps its refusals. The write side is dropped, so no script authors the edit on the agent’s behalf. The trade is stated rather than hidden. Two identical requests may produce two different diffs, and the guardrails and the rendered delta carry the weight a deterministic authoring script would have carried. Prescription 13’s check moves with it and now asks whether a human making the same edit by hand lands in the same jobs and the same rendered delta. (aws-desk , design/agentic , prescriptions ) - Approve the change, not the diff, is prescription 14. The saved
plan is the manifest,
terraform show -jsonrenders the typed changes, the apply job refuses a plan whose digest does not match what was approved, and the PR shows the semantic access delta, meaning the grants added and removed by principal and by resource. Decisions 24 and 31 already say most of this. P14 promotes it to a checkable prescription, closed by lesson I14, whose check is that an apply against a stale or altered plan fails and the PR comment names the grants added and removed. I14 previously claimed P3, which belongs to I2 and I7. (prescriptions , IAM, lesson 14 ) - One permissions boundary for the whole estate. It lives in
access/baseline/as anaws_iam_policyand it denies all IAM write, Organizations and Identity Center, the guardrail-path resources by name, and boundary detachment, while allowing the service surface an app team plausibly needs. That is the lean the delegation note carried, adopted as written. Splitting per OU is deferred until an OU needs it. The Sandbox OU carries no boundary at all, because sandboxes exist to be broken and the live session guide has the room break things there.deployeris not delegable, so a satellite creates onlyserviceroles. The cost is that the boundary becomes the most-revised object in the baseline, and tightening it can break an existing role at apply time where lint will not catch it, so guardrail-rollout’s warn discipline applies. Settles delegation items 1, 2, 4 and 5. (design/delegation , IAM, lesson 5 , demo ) - Break-glass is a temporary Identity Center assignment carrying a
time condition. The assignment’s policy carries an
aws:CurrentTimecondition, so the cloud ends the access even if every job dies. Max TTL is two hours, held inaccess/baseline/as a constant, and a tflint rule refuses a longer one. The approver is the reviewer of the break-glass PR, and the apply job copies that identity into the grant’s tags, so the approval and the artifact name the same human. With the code host down the fallback is a CLI confirmation by a second human, as the break-glass note already documents. The cost is that Floci cannot run Identity Center, so the self-paced path uses the time-conditioned policy on a role and the page says so. Settles break-glass items 1, 2, 3 and 5. Item 4, TEAM interop, stays open. (design/break-glass , IAM, lesson 10 ) - Adopt in place is import from live, one resource at a time. The
documented first path for an existing estate is an
importblock per resource,terraform plan -generate-config-outreviewed by hand into the one-file-per-resource layout, and a plan that proves nothing changes on day one, with aremovedblock to back out. Greenfield is the course’s own path and needs no adoption story. Carve was chant-only (decision 23). Bulk import is not the documented path, because water park manages what it declares (decision 3, Accessible Ops XIII) and a bulk import declares a pile nobody has read. The old export-bundle criterion is moot now that the HCL is the artifact, and decision 2 stands. The cost is that adoption is slow by design and a large estate takes many PRs. (IAM, lesson 15 ) - Workloads federate through declared OIDC providers. The trust
anchors are
aws_iam_openid_connect_providerentries underaccess/identity/, issuer and audience pinned, no wildcardsubclaim. Roles Anywhere gets one paragraph as the option for a fleet with an existing PKI, and nothing more. The rotation check for the few remaining static secrets runs on the same weekday schedule as the watch, so one cron drives both and lesson I13 teaches the schedule once. The cost is that an org whose workloads sit outside a CI or a cluster reads that one paragraph and builds the rest itself. Settles workload-identity item 4 and the rotation cadence. (design/workload-identity , IAM, lesson 9 ) - The watcher holds at most five open PRs. The watcher’s prompt
says so, and the credential-free PR job counts open PRs carrying the
desk marker and fails a sixth, so the cap holds when the prompt is
ignored. No propose endpoint is built, because a job that already
reads the code host can do the counting. Five is a constant in
access/baseline/so lesson I13 can show a student changing it. The cost is that a real backlog takes several cycles to clear and the sixth finding waits. (aws-desk , design/agentic , IAM, lesson 13 ) - The persona set is four, and it is closed. The personas are
reader,deployer,service, and theplatformpersona that owns the guardrail path. That is what this estate needs, and adding one is a module release rather than a leaf-file edit. Team scoping is module parameters. There is no standing admin, because permissions management always goes through the repo. The three-org survey is dropped as out of scope for a course whose estate is one small org, and the personas note says in one sentence that a larger org would revisit the set. Cross-cloud equivalence stays parked with Track B (decision 19). The cost is that a reader running a centralized enterprise gets a set that was never tested against one. Settles personas items 1, 2 and 3. (design/personas , estate , IAM, lesson 2 ) - The access review reads live, never a satellite’s HCL. It reads
the live account through
get-role,list-attached-role-policiesand Access Analyzer unused-access findings, so anything a satellite created appears regardless of which repo declared it. That closes the cross-repo reachability question by not needing an answer to it, which retires delegation item 3 and the archive’s C1 and C6 unknown for this course. The read-only queries,whocan,expiringandoffboard --preview, stay scripts underaccess/scripts/that the desk’s estate pane calls, so there is no separate Q&A page. The cost is that the review is only as current as its last read and it says nothing about a resource nobody has permission to read. Lessons I11 and I8. (design/delegation , design/agentic , IAM, lesson 11 ) - The local backend is the default, and the S3 backend swaps in for
the live path.
access/envs/prod/backend.local.tfis checked in so a fresh clone inits with zero AWS.access/backends/backend.s3.tf(state in waterpark-security, encrypted, withuse_lockfile) is copied in byaccess/scripts/backend s3 envs/prod. Terraform allows one backend block per root, and-backend-configcannot change the backend type, so a file swap is the only clean mechanism. Theflociprovider variable defaults to true for the same reason, the taught path is the default posture, and the live path passes-var floci=false. The provider lock file is gitignored because students run this on three platforms. The cost is that the live path is two commands further from the clone than the local one. (access , IAM, lesson 4 ) - Human principals are live only, under
access/identity/. Floci runs no Identity Center, so permission sets and assignments cannot apply on the solo path. The persona module refuses a human persona in any root that has not setidentity_center = true, and that refusal fires at plan time because Terraform defers cross-variable validation, while a wrong persona name fails at validate.identity/is validated and linted on every check and never applied on a laptop. File names strip the provider prefix from the real type, sossoadmin_permission_set.<name>.tf, not thesso_permission_setthe desk doc sketched, now fixed there too. (access/identity , aws-desk , IAM, lesson 2 ) - Grant policies are rendered by the persona module, not written as
leaf files. A principal file is one module call plus a list of
grants (prescription 2). Lesson I1 still builds the raw role, policy
and attachment as three files so the student sees what the module
replaces in I2, which is why a policy leaf file exists at
checkpoint/i1 and not after, and the desk doc’s sketch of a permanent
one is fixed to match. The module exports a
grantsoutput carrying expiry so a later proofs script has something to read. (access/modules/persona , aws-desk , IAM, lesson 1 , IAM, lesson 2 ) - Leaf files name the boundary once, as
permissions_boundary = module.baseline.boundary_arn. Issue 43 wanted leaf files silent about the boundary. With baseline as a module in the same root, that one reference is the dependency edge that orders the policy before the roles. The alternatives were a second apply with baseline as its own root, or a data-source lookup that fails on first apply. The module still applies the boundary, so no grant restates it. The boundary policy itself is taggedguardrail = "boundary", andno-wildcard-actionexempts it, because a boundary is a ceiling that needss3:*-shaped allows, not a grant. (access/baseline , design/delegation , IAM, lesson 5 ) - The rule pack is tflint with the OPA ruleset, Rego under
access/.tflint.d/policies/, and severity is the function-name prefix.deny_is an error andwarn_a warning, so promoting a rule (decision 9’s warning-first rollout) is a one-word edit plus a line inaccess/scripts/check. All nine rules are Rego, including the two layout rules, because the ruleset exposes each resource’s file name.no-open-ingressandsg-reference-not-cidrship as warnings with fixtures although the estate declares no security groups yet. tflint installs from the tapterraform-linters/tap/tflint, not homebrew core. The editor half of prescription 4 is documented rather than demonstrated, sincetflint --langserveris a process apart fromterraform-ls. (access , design/guardrail-rollout , IAM, lesson 3 ) - What phase 1 for I1 to I5 deliberately left out. ECR and
security groups are not declared in
envs/prod, because the Floci provider overrides only iam, sts and s3 and the fork’s ECR support is untested, so the runner registry and the default-deny groups wait for a lesson that can show them.scripts/proofs,render-delta, CODEOWNERS generation and the apply workflow belong to I6 and later. Access Analyzer validate-policy is untried. Checkpoint tagscheckpoint/i0tocheckpoint/i5mark the repo after each lesson, so lesson N starts from i(N-1). (plan , issues ) - The satellite lives in this repo.
waterpark-runnernames the one satellite, a sibling root underaccess/, named inaccess/README.md. It declares its registry and therunner-builderrole inside the boundary, with no human principal. It is not a second GitHub repository. Decision 33 already made this repo the estate, one clone and one PR flow, and a second repository would put the marquee double-refusal lesson behind a repo the student does not have. The satellite shares CI and CODEOWNERS with the central repo, so the cost is that the lesson has to say what a separate repo would change (its own PR job, its own deploy credential minted centrally), and the double refusal is demonstrated with a separate deploy credential rather than a separate repo. (estate , design/delegation , IAM, lesson 8 ) - The shared module is a git source, not a registry module. Issue 46
and decision 10 said
waterpark/workload-role/aws. The satellite consumes the module asgit::https://github.com/INTENTIUS/waterpark.git//access/modules/<name>?ref=<tag>, pinned to a checkpoint or release tag, and the same for the rule pack path. A registry namespace is a thing to run and an account to hold, and a git ref carries the same version pin. This also answers C1’s open question about how a satellite consumes central identifiers, with module outputs and pinned refs rather thanterraform_remote_state. The cost is that a git source has no semantic version constraint syntax, so warn-minor and error-major (decision 9) is a tagging convention and a line in the rule pack’s README rather than a registry feature. (issues , design/delegation , design/guardrail-rollout , IAM, lesson 8 ) - The apply job runs against Floci, like everything else. I6’s gated apply on protected branches is a GitHub Actions job whose target is a Floci service container in the same job, so the whole path from PR to apply runs with no AWS account, and the digest check on the saved plan (P14) is what the job proves. The OIDC tiers into a real account (decision 12) are declared as code and described on the page, and their first run is live-only until an account exists. The cost is that an apply against a service container that dies with the job proves the pipeline, not persistence, so the lesson says which, and the drift lesson (I7) seeds its drift locally rather than in CI. (threat-model , IAM, lesson 6 , IAM, lesson 7 )
- The plan digest travels as a workflow artifact keyed by the PR head
sha. The pr job uploads
plan.jsonandplan.digestasaccess-plan-<head sha>, the apply job resolves the merged PR from the commit, finds the successful pr run on that head sha and downloads by run id, then recomputes the digest on its own plan and refuses on mismatch. A committed digest file was rejected because a digest is against a particular account, and a student whose Floci already holds the estate computes a different one from an empty CI container. One script,access/scripts/plan-digest, does the normalization for both jobs. The cost is that a rebase merge changes the head sha, so the apply job must resolve the PR through the merged commit rather than the sha it runs on, and a PR merged without a green pr run has no artifact and the apply refuses, which is the intended failure. (access , IAM, lesson 6 ) - The shared module for satellites is
access/modules/persona, not aworkload_rolewrapper. Decision 10 named the wrapper. A wrapper forwarding a dozen variables declares them twice and enforces nothing the boundary and the rule pack do not already enforce. The persona module gainedfederated_trustwith an explicit issuer host so trust policies are known at plan time, and apushgrant level for registries withecr:GetAuthorizationTokenas its own statement because the API refuses to scope it. The satellite consumes the boundary through adata "aws_iam_policy"lookup by name, notterraform_remote_stateand not a copied ARN. (access/modules/persona , design/delegation , IAM, lesson 8 ) - The OIDC provider for GitHub Actions lives in
envs/prod, notidentity/. Decision 39 put trust anchors underidentity/, butidentity/targets the management account and is live only, while an OIDC provider is account scoped and this one belongs towaterpark-prodbeside the role that trusts it. Lesson I9 settles whetheridentity/keeps any trust anchors at all. (design/workload-identity , IAM, lesson 6 , IAM, lesson 9 ) runner-buildermoved fromenvs/prodto the satellite rootaccess/satellites/waterpark-runner/, per the estate ’s scenario 5. The satellite declares its own ECR registry, because the patched Floci runs ecr andaws_ecr_repositoryapplies and plans clean with an endpoint override. This lifts the registry deferral in decision 48. The module source is committed as the local path, withaccess/scripts/satellite-source local|git <tag>switching to the pinned git form, because the tag a satellite pins is cut after the commit that introduces it. Earlier checkpoints are unaffected, since they are tags of the tree as it was. (estate , design/delegation , IAM, lesson 8 )- Two rule changes.
path-matches-namedrops any known provider prefix (aws_,github_), andboundary-requiredalso fires on a module call in aniam_role.*.tffile, because tflint reads the calling directory only and a satellite leaf file with the boundary stripped would otherwise pass lint. The double refusal depends on the second. Both carry failing and passing fixtures. Both landed atcheckpoint/i6with the check stack rather than at lesson 8, so lesson 6 is where the first refusal became real and lesson 8 relies on it. (design/guardrail-rollout , IAM, lesson 6 , IAM, lesson 8 ) - The rule pack is delivered to a satellite by a shallow clone at a tag,
not a tflint plugin source. tflint has no git source for a Rego pack,
so the satellite’s
.tflint.hclsetsTFLINT_OPA_POLICY_DIRand the documented clone at depth 1 on the tag is the pin. Warn-minor and error-major is a tagging convention written inaccess/.tflint.d/README.md. Ratchet baselines for what already violates a new rule are not built, and the README says so. (design/guardrail-rollout , IAM, lesson 8 ) - The apply role’s boundary is a known contradiction that only bites on
a real account. The estate boundary denies all IAM write and the
guardrail path by name, which includes
role/waterpark-applyand the state bucket, so awaterpark-applycarrying it could not apply the estate. On the taught path the job uses Floci test credentials and the role carries no grants, andaccess/scripts/prove-no-detachuses a stand-in user because Floci honors the role’s trust policy. A real account needs an apply-specific boundary, which decision 36’s one-boundary rule has not taken. Also record here as facts, not decisions. Floci does enforce boundary denies in authorization (phase 0 tested only the condition key), Floci honors trust policies onsts:AssumeRole, the drift job in CI is always clean because it applies into an empty container and says so, and the satellite deploy credentialwaterpark-runner-deployis a user with the boundary condition as its cap and no boundary of its own, standing in for the satellite’s OIDC role since Floci has no subject to bind. (threat-model , IAM, lesson 6 , IAM, lesson 8 )