feat: D19 bounds the review loop; apply nine rulings; close Q46(a) and Q48–Q53
D19 into §3 and swept to all six destinations Pouya named: both agent briefs
(scope + severity filter), /build Phases 2–4 (non-gating questions, scope, stop
signal, two-round cap), /wrap, and CLAUDE.md (comments record decisions, not
history). Sweep verified by command, not recalled.
Rulings applied:
Q46(a) PUBLISHABLE — three §4 Offerings rows flipped; gate 1 records
[Pouya's stated basis], never [verified]. Step 5 unblocked.
Q48 closed, not site-relevant.
Q49 one §4 row, "Mediator"; read as declining Q49(b), so worksFor stays out.
Q50 DEVIATES — ships as name + slogan, not the concatenation. Flagged.
Q51 OBA sections stay; the regulator/voluntary distinction recorded.
Q52 docs/reference/deploy-credential-verification.md — 18 read-only AWS
calls, re-run rather than transcribed, access key ID redacted.
Q53 memberOf emitted on /about/'s Person node.
Two review rounds. The headline finding was this session's own: the Q53 sweep
was asserted and never run, leaving six in-scope records saying memberOf was
withheld — including §12 R10, which is read aloud every session. Round 2 then
found that round 1's simplification had put memberOf on / as well; the per-page
opt-in is restored, because Pouya's ruling turns on /about/'s visible HTML.
Also fixed: MEMBERSHIP_ORGS had orphaned BOUTIQUE's D16 JSDoc; /'s title now
derives from the constants; §7's deploy row stated and retracted three facts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148NztQskLKKApP5SzAA78e
This commit is contained in:
co-authored by
Claude Opus 5
parent
77a7b410b2
commit
fd5f610982
+18
-11
@@ -99,7 +99,7 @@ Revisit at month 12–18, once there is relationship history to point to.
|
||||
|
||||
## Not a practice area yet: tax-adjacent disputes
|
||||
|
||||
**Canadian Tax Foundation membership is verified** (`AGENTS.md` §4, 2026-08-26)
|
||||
**Canadian Tax Foundation membership is verified** (`AGENTS.md` §4 — read the stamp there; a second copy of a currency stamp goes stale where nobody re-reads it, and this one had)
|
||||
and it is the one credential none of the six areas above touch. Tax-adjacent
|
||||
disputes are genuinely ADR territory — valuation and purchase-price disputes on
|
||||
a share sale, indemnity and earn-out fights that turn on a tax position,
|
||||
@@ -260,17 +260,24 @@ Six cards, one paragraph each, linking onward. Also the natural home for the
|
||||
pre-dispute technical advisory** — three, and each has an `AGENTS.md` §4
|
||||
Offerings row.
|
||||
|
||||
> 🚫 **THE STRIP MAY NOT SHIP YET, AND HAVING A ROW IS NOT WHAT UNBLOCKS IT.**
|
||||
> All three rows read **"GATES 0 AND 2 ANSWERED; GATE 1 STILL HAS NO SOURCE —
|
||||
> NOT YET PUBLISHABLE"**. Pouya ruled on 2026-08-28 that all three pass; §4's
|
||||
> gate 1 asks *is the activity gated?*, which the register answers with a
|
||||
> checkable source and not with a ruling, and **no source exists for any of the
|
||||
> three**. Tracked as **Q46(a)**, which names this strip as the thing it blocks.
|
||||
> ✅ **THE STRIP MAY SHIP — Q46(a) closed 2026-08-28, on Pouya's second
|
||||
> ruling.** All three §4 Offerings rows now read **PUBLISHABLE**. Read the ENE
|
||||
> row before writing the strip: gate 1 (*is the activity gated?*) is closed as
|
||||
> **`[Pouya's stated basis 2026-08-28]`** and **not** as `[verified]`, because no
|
||||
> source for any of the three exists in `docs/reference/`. That is a deliberate,
|
||||
> attributed position of the architect's, not a sourced fact — do not describe it
|
||||
> on the page or in a comment as settled law.
|
||||
>
|
||||
> This paragraph read *"each now has a §4 Offerings row, which is what the strip
|
||||
> needs before it may ship"* until 2026-08-28 — **false as stated**, and it would
|
||||
> have had an implementer at step 5 ship three offerings the register marks
|
||||
> unpublishable. Found by `claims-auditor` on re-audit.
|
||||
> **Two framing constraints survive the ruling and are not discretionary:**
|
||||
>
|
||||
> 1. **ENE is an assessment delivered to BOTH parties**, never advice to one. It
|
||||
> is the offering nearest §4's NOT-NEGOTIABLE boundary — a neutral assessment
|
||||
> of the *merits* sits closest to *"providing legal services"*.
|
||||
> 2. **Pre-dispute advisory carries a conflict caution**, and it is practice
|
||||
> management rather than a publication gate: advisory work for one
|
||||
> organisation can conflict against a later appointment in the same matter.
|
||||
> **No copy may imply the offering is free of that tension** — this strip is
|
||||
> where the temptation to imply it will arise.
|
||||
|
||||
> **`settlement counsel` IS STRUCK FROM THIS STRIP AND MUST NOT BE RESTORED.**
|
||||
> `AGENTS.md` Q42, 2026-08-27. Pouya struck it as his own error in this document:
|
||||
|
||||
+7
-3
@@ -48,8 +48,12 @@ D1.
|
||||
Every page passes through one `SEO` component. A page without it is not finished.
|
||||
|
||||
```
|
||||
title 50–60 chars, unique. Pattern: "<Page> · Pouya Lajevardi"
|
||||
Home: "Pouya Lajevardi · Mediation & Arbitration · Toronto"
|
||||
title 50–60 chars, unique. Pattern: "<Page> · Pouya Lajevardi".
|
||||
`/` composes its own from the constants — `SITE.name` +
|
||||
`SITE.tagline` — rather than a literal, so the masthead and
|
||||
the title cannot drift. This spec carried the literal with an
|
||||
ampersand after the composition shipped with interpuncts;
|
||||
cite the constants, do not restate them (AGENTS.md §7 rule).
|
||||
ARTICLES ARE THE EXCEPTION: no " · Pouya Lajevardi" suffix.
|
||||
The suffix is 18 chars, so a headline that already reads
|
||||
50–60 renders at 68–78 — over this ceiling. Measured against
|
||||
@@ -101,7 +105,7 @@ JSON-LD only. Validate against Google's Rich Results Test before cutover.
|
||||
|
||||
| Type | Where | Notes |
|
||||
|---|---|---|
|
||||
| `Person` | `/about/`, referenced site-wide | **Emitted:** `name`, `url`, `jobTitle`, `description`, `alumniOf` (Bond University), `knowsLanguage` (en, fa), `hasCredential` (Q.Med), `sameAs` (LinkedIn), `email`, `image`. **Withheld:** `worksFor`, `memberOf`. *(This enumeration listed `worksFor` as emitted while the same cell said it was withheld, and omitted `url` and `email`, which are — wrong in both directions. The enumeration is the part an implementer copies. Found by `adversarial-reviewer`.)* **CHANGED 2026-08-28 — Q47.** This row read *"`jobTitle` = 'Director of Firm Operations'; omit `worksFor`"*, which put the boutique title on a node whose `url` is this ADR practice's `/about/` — so a consumer could attach it to this entity. Pouya's ruling reframes the field: `jobTitle` describes **this practice**, not the boutique role, which D16 keeps unnamed. The visible role line is unchanged and still reads "Director of Firm Operations at a Toronto litigation and ADR boutique". **THE VALUE IS `PRACTICE_JOB_TITLE` IN `src/data/site.ts` AND THIS ROW DOES NOT RESTATE IT** — §7's rule, applied to a string with a live revert trigger on it: this row carried the literal text for one pass, and `adversarial-reviewer` noted it would go stale the moment the constant moved. Cite, do not copy. **`worksFor` IS WITHHELD** — set for one pass under Q47, then reverted: `ProfessionalService.provider` is this Person, so `provider → Person → worksFor` asserts the same-entity claim `schema.ts` explicitly declines, and §4 says "alongside the practice" where the ruling says "operates through". **`memberOf` is withheld too**, on volatility grounds, even though Q44 closed. Both are **Q49**. See `src/data/schema.ts` |
|
||||
| `Person` | `/about/`, referenced site-wide | **Emitted:** `name`, `url`, `jobTitle`, `description`, `alumniOf` (Bond University), `knowsLanguage` (en, fa), `hasCredential` (Q.Med), `sameAs` (LinkedIn), `email`, `image`. **Emitted on `/about/` only:** `memberOf` — the four §4 memberships as `Organization` nodes (Q53, ruled 2026-08-28). `/` shows no memberships, so its Person node omits it: structured data represents the page it sits on. **Withheld:** `worksFor` — Q49(b) declined the row 2026-08-28; `provider → Person → worksFor` would assert a same-entity claim §4 does not row. *(This enumeration listed `worksFor` as emitted while the same cell said it was withheld, and omitted `url` and `email`, which are — wrong in both directions. The enumeration is the part an implementer copies. Found by `adversarial-reviewer`.)* **CHANGED 2026-08-28 — Q47.** This row read *"`jobTitle` = 'Director of Firm Operations'; omit `worksFor`"*, which put the boutique title on a node whose `url` is this ADR practice's `/about/` — so a consumer could attach it to this entity. Pouya's ruling reframes the field: `jobTitle` describes **this practice**, not the boutique role, which D16 keeps unnamed. The visible role line is unchanged and still reads "Director of Firm Operations at a Toronto litigation and ADR boutique". **THE VALUE IS `PRACTICE_JOB_TITLE` IN `src/data/site.ts` AND THIS ROW DOES NOT RESTATE IT** — §7's rule, applied to a string with a live revert trigger on it: this row carried the literal text for one pass, and `adversarial-reviewer` noted it would go stale the moment the constant moved. Cite, do not copy. **`worksFor` IS WITHHELD** — set for one pass under Q47, then reverted: `ProfessionalService.provider` is this Person, so `provider → Person → worksFor` asserts the same-entity claim `schema.ts` explicitly declines, and §4 says "alongside the practice" where the ruling says "operates through". **`memberOf` is emitted** — see the sentence above; Q53 closed 2026-08-28. *(This cell asserted `memberOf` was both emitted and withheld for one pass, which is the defect it already records itself being caught for on `worksFor`, in the opposite direction. The enumeration is the part an implementer copies.)* See `src/data/schema.ts` |
|
||||
| `ProfessionalService` | Home | `areaServed` Toronto/Ontario, `serviceType` **Mediation / Commercial arbitration / Mediation-arbitration (med-arb)** — *scoped 2026-08-28 on `claims-auditor`'s finding; this row instructed the unscoped class form "Mediation/Arbitration" that Q39 struck and that `schema.ts` deliberately does not follow. Family arbitration carries prescribed training and has its own NOT OFFERED row, so unscoped "Arbitration" is the struck universal in a field nobody reads. Do not widen these strings without a §4 row to widen them from* — `provider` → Person, `priceRange` once `/fees/` is real. **Never `LegalService`** — schema.org defines it as a business providing legal advice and *representation*, which asserts in machine-readable form exactly what D13 bars and §4 Forbidden calls out |
|
||||
| `Service` | Each practice page | `serviceType`, `provider` → Person, `areaServed` |
|
||||
| `Article` | Each article | `headline`, `description`, `datePublished`, `dateModified`, `author` → Person, `image` |
|
||||
|
||||
+13
-25
@@ -356,34 +356,22 @@ Then invalidate `/*`.
|
||||
**Do NOT add a currency sentence to the page while you are here** — his
|
||||
ruling is *"list the memberships; promise nothing about their future
|
||||
state"*, and the struck sentence stays struck.
|
||||
**Do NOT add `memberOf` to the Person JSON-LD** unless Pouya has said to.
|
||||
It is withheld deliberately and on narrower grounds than the visible page:
|
||||
the reason is recorded in `src/data/schema.ts` and **it is an open
|
||||
judgement, not a settled one — `AGENTS.md` Q53.** Do not restate the
|
||||
reasoning here; a third copy is how the first two went stale.
|
||||
**`memberOf` IS EMITTED on `/about/`** — Q53 answered 2026-08-28 and the
|
||||
withholding is dropped, so the graph asserts the same four memberships the
|
||||
page shows. **This item therefore covers both**: re-confirming before
|
||||
cutover means `src/data/schema.ts` as well as the visible list, and they
|
||||
must not be allowed to diverge.
|
||||
**§4 records yearly renewal for the OBA sections and the CTF only** — it
|
||||
says nothing about ADRIC's or ADRIO's period, and an earlier version of
|
||||
this line asserted "all renew yearly", which §4 does not support.
|
||||
- [ ] **`AGENTS.md` Q51 answered before cutover** — does listing the **OBA
|
||||
sections** on `/about/` carry the licensure implication that excludes the
|
||||
LSO? §4 excludes the Law Society precisely because *"listing the Law Society
|
||||
among memberships implies licensure"*, and Forbidden bars *any phrasing that
|
||||
**implies** entitlement to practise law*. The OBA sections **went live on
|
||||
`/about/` on 2026-08-28** and Q51 was raised the same day, **after** Pouya's
|
||||
Q44 ruling authorised the group — so the ruling did not consider it.
|
||||
`adversarial-reviewer` rates it moderate-low confidence as a defect and
|
||||
**high confidence that the question is unasked**: `grep -n OBA AGENTS.md`
|
||||
returns rows on renewal, scope and stamping and nothing on implication, and
|
||||
OBA membership eligibility is not established anywhere in this repo. It sits
|
||||
beside **R1** — same subject, same page, and not settleable internally. If
|
||||
the answer is yes, the fix is the LSO fix: exclude deliberately, and record
|
||||
that it was excluded rather than omitted.
|
||||
*(This item did not exist until 2026-08-28. Q48, which governs nothing
|
||||
currently on the site, had a checklist item while Q51, which governs live
|
||||
public copy, had none.)*
|
||||
**Also re-check `AGENTS.md` Q48** before re-stamping: if `Q.Med` retention
|
||||
turns out to depend on ADRIO membership currency, this item covers the
|
||||
site's central credential and not just a list
|
||||
- [ ] **The OBA sections stay listed; the LSO stays out** — a check that nobody
|
||||
has tidied the two into one list, not an open question. `AGENTS.md` **Q51
|
||||
answered 2026-08-28**: the Law Society is the **regulator**, so membership
|
||||
*is* licensure; the OBA is a **voluntary association**, which admits
|
||||
members it does not license. Structural, and independent of eligibility
|
||||
details — which is what made the question unanswerable inside this repo
|
||||
before the ruling. **R1 is still live**: same page, same subject, different
|
||||
question
|
||||
- [ ] No `TODO(pouya)` remains in any shipped page
|
||||
- [ ] No matter counts, rates, dollar figures, or testimonials anywhere
|
||||
- [ ] Q.Arb described as **commenced August 2026** everywhere it appears — §4's
|
||||
|
||||
+8
-10
@@ -133,16 +133,14 @@ offer tribunal-secretary work on the site.
|
||||
Early neutral evaluation, dispute-system design, and pre-dispute technical
|
||||
advisory: **$500 / hour**.
|
||||
|
||||
> 🚫 **A RATE IS NOT A PUBLICATION LICENCE, AND THESE THREE ARE NOT PUBLISHABLE
|
||||
> YET.** §4's ENE row says the pricing here *"is a fee-page question, not a
|
||||
> publication licence"*, and all three Offerings rows read **NOT YET
|
||||
> PUBLISHABLE** — gate 1 (*is the activity gated?*) has no source. **Q46(a)**
|
||||
> names `docs/07-fees.md` pricing directly as something it blocks.
|
||||
>
|
||||
> So: the rate is recorded and settled; **the line item does not go on `/fees/`
|
||||
> until Q46(a) closes.** This note did not exist until 2026-08-28, which meant
|
||||
> the gate lived only in `AGENTS.md` while this file says "Build `/fees/` from
|
||||
> it". Found by `claims-auditor` on re-audit.
|
||||
> ✅ **PUBLISHABLE — Q46(a) closed 2026-08-28.** All three §4 Offerings rows
|
||||
> read PUBLISHABLE on Pouya's second ruling, so the line item may go on
|
||||
> `/fees/`. **Read the §4 ENE row first:** gate 1 is closed as **`[Pouya's
|
||||
> stated basis 2026-08-28]`**, not `[verified]` — there is still no source in
|
||||
> `docs/reference/` — and the framing constraints in `docs/01` §`/practice/`
|
||||
> travel with the offering onto this page. In particular, **ENE is priced as an
|
||||
> assessment delivered to both parties**, and nothing on `/fees/` may read as a
|
||||
> rate for advising one of them.
|
||||
|
||||
**THREE services, not four. `settlement counsel` is struck and must not be
|
||||
priced** — `AGENTS.md` Q42, Pouya 2026-08-27, correcting his own entry in
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
# Deploy credential — verification output
|
||||
|
||||
**What this is.** The tool output behind `AGENTS.md` §7's *Deploy credential —
|
||||
PROVISIONED* row and the two risk downgrades in `AGENTS.md` §10. It exists because the row
|
||||
was written on evidence that lived only in a terminal, which is the
|
||||
**Q24 / Q32 shape** R14 exists for: a claim whose supporting artefact is
|
||||
unreachable is unverifiable by construction, not merely unverified.
|
||||
`AGENTS.md` **Q52**, ruled 2026-08-28: *"YES — commit the simulate results, the
|
||||
inline policy, and the `NoSuchBucketPolicy` response, access key ID redacted."*
|
||||
|
||||
## Provenance
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Subject | IAM user `adr-sml-deploy`, AWS account `327082975128` |
|
||||
| Retrieved | **2026-08-28**, re-run from the repository root |
|
||||
| Calling identity | `arn:aws:iam::327082975128:user/pouya` — the broadly-permissioned personal user, **at an interactive keyboard**. `AGENTS.md` §7 and §10: acceptable interactively, **never** as a CI credential |
|
||||
| Calls | 18 — all **read-only**. No `create`, `put`, `attach`, `delete`, or `update` |
|
||||
| Redaction | The access key ID is replaced with `AKIA…REDACTED` throughout. Nothing else is redacted; `UserId` (`AIDA…`) is retained because it is the durable principal identifier that appears in CloudTrail and grants nothing. The **secret** access key was never requested and cannot be retrieved from the API at all |
|
||||
| Method | `aws-cli/2.34.53`. Every command's exit status was read, and **no `2>/dev/null` anywhere** — see the `NoSuchBucketPolicy` section, where the error *is* the result |
|
||||
|
||||
**Re-run it.** Every command below is copy-pasteable. Nothing here is
|
||||
transcribed from a session; this file was generated from the captured output.
|
||||
|
||||
---
|
||||
|
||||
## 1. The user
|
||||
|
||||
```console
|
||||
$ aws iam get-user --user-name adr-sml-deploy
|
||||
{
|
||||
"User": {
|
||||
"Path": "/",
|
||||
"UserName": "adr-sml-deploy",
|
||||
"UserId": "AIDAUYJ5KD6MHAAC4BOKD",
|
||||
"Arn": "arn:aws:iam::327082975128:user/adr-sml-deploy",
|
||||
"CreateDate": "2026-08-26T15:45:18+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Created **2026-08-26T15:45:18+00:00**, path `/`, no tags.
|
||||
|
||||
## 2. Nothing attached, no groups, one inline policy
|
||||
|
||||
```console
|
||||
$ aws iam list-attached-user-policies --user-name adr-sml-deploy
|
||||
{
|
||||
"AttachedPolicies": []
|
||||
}
|
||||
|
||||
$ aws iam list-groups-for-user --user-name adr-sml-deploy
|
||||
{
|
||||
"Groups": []
|
||||
}
|
||||
|
||||
$ aws iam list-user-policies --user-name adr-sml-deploy
|
||||
{
|
||||
"PolicyNames": [
|
||||
"adr-sml-deploy-minimal"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**This is the part policy-reading alone gets wrong.** An empty
|
||||
`AttachedPolicies` proves nothing on its own — permissions can arrive through a
|
||||
group, or through a resource-based policy that is invisible from the IAM side.
|
||||
Both are checked: `Groups` is empty here, and **§6 below** checks the resource
|
||||
side — §5 is the identity-side simulation and does not reach a bucket policy.
|
||||
|
||||
## 3. The inline policy — `adr-sml-deploy-minimal`
|
||||
|
||||
```console
|
||||
$ aws iam get-user-policy --user-name adr-sml-deploy \
|
||||
--policy-name adr-sml-deploy-minimal
|
||||
{
|
||||
"UserName": "adr-sml-deploy",
|
||||
"PolicyName": "adr-sml-deploy-minimal",
|
||||
"PolicyDocument": {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "ListSiteBucket",
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:ListBucket",
|
||||
"Resource": "arn:aws:s3:::adr-smlcompany-site"
|
||||
},
|
||||
{
|
||||
"Sid": "WriteSiteObjects",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Resource": "arn:aws:s3:::adr-smlcompany-site/*"
|
||||
},
|
||||
{
|
||||
"Sid": "InvalidateOneDistribution",
|
||||
"Effect": "Allow",
|
||||
"Action": "cloudfront:CreateInvalidation",
|
||||
"Resource": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Four actions, one bucket, one distribution. **Never widen it** — `AGENTS.md` §10
|
||||
records why
|
||||
in terms: this is the control standing between a shared Gitea instance and an
|
||||
AWS account that also holds `mlp-clientdb-prod-backups-327082975128`. Treat any
|
||||
request to widen it as a security decision, not a convenience one.
|
||||
|
||||
## 4. The access key — issued, never used
|
||||
|
||||
```console
|
||||
$ aws iam list-access-keys --user-name adr-sml-deploy
|
||||
{
|
||||
"AccessKeyMetadata": [
|
||||
{
|
||||
"UserName": "adr-sml-deploy",
|
||||
"AccessKeyId": "AKIA…REDACTED",
|
||||
"Status": "Active",
|
||||
"CreateDate": "2026-08-26T15:45:19+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
$ aws iam get-access-key-last-used --access-key-id AKIA…REDACTED
|
||||
{
|
||||
"UserName": "adr-sml-deploy",
|
||||
"AccessKeyLastUsed": {
|
||||
"ServiceName": "N/A",
|
||||
"Region": "N/A"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
One key, `Active`, created **2026-08-26T15:45:19+00:00**.
|
||||
|
||||
⚠️ **A correction to `AGENTS.md` §7's wording, found by re-running the call.**
|
||||
That row said
|
||||
*"one active access key, `LastUsed` null"*. The API does not return null — it
|
||||
returns `AccessKeyLastUsed` with **`ServiceName: "N/A"` and `Region: "N/A"`**,
|
||||
and there is no `LastUsedDate` key at all. Same conclusion (the key has never
|
||||
authenticated a request), different field shape, and a reader looking for a
|
||||
literal `null` would not find one.
|
||||
|
||||
**Rotation is due 2026-11-26** — key created 2026-08-26, D3 commits to
|
||||
quarterly. `AGENTS.md` §12 R17 carries the date; `docs/06-deployment.md` §Key rotation
|
||||
carries the procedure. Create the second key, update the Gitea secrets, verify a
|
||||
deploy, **then** delete the first.
|
||||
|
||||
## 5. `simulate-principal-policy` — nine checks, verified by execution
|
||||
|
||||
The point of this section is that it tests the **negative** half. A policy that
|
||||
grants four actions is only a control if it can be shown not to grant the rest.
|
||||
|
||||
```bash
|
||||
PRINCIPAL=arn:aws:iam::327082975128:user/adr-sml-deploy
|
||||
sim() { aws iam simulate-principal-policy \
|
||||
--policy-source-arn "$PRINCIPAL" --action-names "$1" --resource-arns "$2" ; }
|
||||
```
|
||||
|
||||
| # | Action | Resource | `EvalDecision` |
|
||||
|---|---|---|---|
|
||||
| 1 | `s3:PutObject` | `arn:aws:s3:::adr-smlcompany-site/index.html` | **allowed** |
|
||||
| 2 | `s3:DeleteObject` | `arn:aws:s3:::adr-smlcompany-site/old.html` | **allowed** |
|
||||
| 3 | `s3:ListBucket` | `arn:aws:s3:::adr-smlcompany-site` | **allowed** |
|
||||
| 4 | `cloudfront:CreateInvalidation` | `arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA` | **allowed** |
|
||||
| 5 | `s3:ListBucket` | `arn:aws:s3:::mlp-clientdb-prod-backups-327082975128` | **implicitDeny** |
|
||||
| 6 | `s3:GetObject` | `arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything` | **implicitDeny** |
|
||||
| 7 | `s3:DeleteBucket` | `arn:aws:s3:::adr-smlcompany-site` | **implicitDeny** |
|
||||
| 8 | `s3:PutObject` | `arn:aws:s3:::meshkinilaw.ca/anything` | **implicitDeny** |
|
||||
| 9 | `iam:CreateUser` | `*` | **implicitDeny** |
|
||||
|
||||
Four `allowed`, five `implicitDeny`. `implicitDeny` rather than `explicitDeny`
|
||||
is the correct and expected shape: the policy contains no `Deny` statement, so
|
||||
everything outside its three `Allow` statements falls through to IAM's
|
||||
default-deny. An `explicitDeny` here would mean a *different* policy was also in
|
||||
play.
|
||||
|
||||
Raw output for each check, including the full `MatchedStatements` and
|
||||
`MissingContextValues`, follows in **§7 of this file**. *(Bare `§n` here means a
|
||||
section of this document; references to the working file are written
|
||||
`AGENTS.md` §n.)*
|
||||
|
||||
## 6. The resource side — `NoSuchBucketPolicy`, and why the contrast call matters
|
||||
|
||||
IAM simulation covers identity-based policy. It does **not** cover a
|
||||
resource-based grant on the backup bucket, which would be invisible from the
|
||||
principal's side. So:
|
||||
|
||||
```console
|
||||
$ aws s3api get-bucket-policy --bucket mlp-clientdb-prod-backups-327082975128
|
||||
# exit status: 254
|
||||
# stdout: 0 bytes
|
||||
# stderr:
|
||||
# aws: [ERROR]: An error occurred (NoSuchBucketPolicy) when calling the GetBucketPolicy operation: The bucket policy does not exist
|
||||
```
|
||||
|
||||
**An error is the result here, and that is only legible because stderr was not
|
||||
suppressed.** `CLAUDE.md`'s convention, from Pouya's own 2026-08-28 correction:
|
||||
`2>/dev/null` converts *"it failed"* into *"it found nothing"*, and those are
|
||||
opposite results. The exit status is **254** and stdout is **empty** — read the
|
||||
status, not just stdout.
|
||||
|
||||
**And an empty result needs a working instrument.** The same command against the
|
||||
site bucket, to prove the call itself resolves a policy when one exists:
|
||||
|
||||
```console
|
||||
$ aws s3api get-bucket-policy --bucket adr-smlcompany-site
|
||||
{
|
||||
"Policy": "{\"Version\":\"2008-10-17\",\"Id\":\"PolicyForCloudFrontPrivateContent\",\"Statement\":[{\"Sid\":\"AllowCloudFrontServicePrincipal\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cloudfront.amazonaws.com\"},\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::adr-smlcompany-site/*\",\"Condition\":{\"StringEquals\":{\"AWS:SourceArn\":\"arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA\"}}}]}"
|
||||
}
|
||||
```
|
||||
|
||||
Exit **0**, a real policy returned — the CloudFront OAC grant, scoped to
|
||||
distribution `E1OK7G98KNKUTA`. So `NoSuchBucketPolicy` on the backup bucket is a
|
||||
**genuine absence**, not a command that failed to run. That contrast is the
|
||||
whole reason this call is in the file: without it, an empty result from a broken
|
||||
command looks exactly like an empty result from a bucket with no policy.
|
||||
|
||||
*(This contrast call was **not** part of the original Q22 verification. It was
|
||||
added on re-run, and it closes the gap that made the original evidence weaker
|
||||
than it read.)*
|
||||
|
||||
## 7. Raw `simulate-principal-policy` responses
|
||||
|
||||
### Check 1
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "s3:PutObject",
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/index.html",
|
||||
"EvalDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 6,
|
||||
"Column": 55
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 9,
|
||||
"Column": 57
|
||||
}
|
||||
}
|
||||
],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/index.html",
|
||||
"EvalResourceDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 6,
|
||||
"Column": 55
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 9,
|
||||
"Column": 57
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 2
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "s3:DeleteObject",
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/old.html",
|
||||
"EvalDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 6,
|
||||
"Column": 55
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 9,
|
||||
"Column": 57
|
||||
}
|
||||
}
|
||||
],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/old.html",
|
||||
"EvalResourceDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 6,
|
||||
"Column": 55
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 9,
|
||||
"Column": 57
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 3
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "s3:ListBucket",
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||
"EvalDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 3,
|
||||
"Column": 17
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 6,
|
||||
"Column": 55
|
||||
}
|
||||
}
|
||||
],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||
"EvalResourceDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 3,
|
||||
"Column": 17
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 6,
|
||||
"Column": 55
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 4
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "cloudfront:CreateInvalidation",
|
||||
"EvalResourceName": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA",
|
||||
"EvalDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 9,
|
||||
"Column": 57
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 12,
|
||||
"Column": 83
|
||||
}
|
||||
}
|
||||
],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA",
|
||||
"EvalResourceDecision": "allowed",
|
||||
"MatchedStatements": [
|
||||
{
|
||||
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
|
||||
"SourcePolicyType": "IAM Policy",
|
||||
"StartPosition": {
|
||||
"Line": 9,
|
||||
"Column": 57
|
||||
},
|
||||
"EndPosition": {
|
||||
"Line": 12,
|
||||
"Column": 83
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 5
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "s3:ListBucket",
|
||||
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128",
|
||||
"EvalDecision": "implicitDeny",
|
||||
"MatchedStatements": [],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128",
|
||||
"EvalResourceDecision": "implicitDeny"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 6
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "s3:GetObject",
|
||||
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything",
|
||||
"EvalDecision": "implicitDeny",
|
||||
"MatchedStatements": [],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything",
|
||||
"EvalResourceDecision": "implicitDeny"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 7
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "s3:DeleteBucket",
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||
"EvalDecision": "implicitDeny",
|
||||
"MatchedStatements": [],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
|
||||
"EvalResourceDecision": "implicitDeny"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 8
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "s3:PutObject",
|
||||
"EvalResourceName": "arn:aws:s3:::meshkinilaw.ca/anything",
|
||||
"EvalDecision": "implicitDeny",
|
||||
"MatchedStatements": [],
|
||||
"MissingContextValues": [],
|
||||
"EvalDecisionDetails": {},
|
||||
"ResourceSpecificResults": [
|
||||
{
|
||||
"EvalResourceName": "arn:aws:s3:::meshkinilaw.ca/anything",
|
||||
"EvalResourceDecision": "implicitDeny"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check 9
|
||||
|
||||
```json
|
||||
{
|
||||
"EvaluationResults": [
|
||||
{
|
||||
"EvalActionName": "iam:CreateUser",
|
||||
"EvalResourceName": "*",
|
||||
"EvalDecision": "implicitDeny",
|
||||
"MatchedStatements": [],
|
||||
"MissingContextValues": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What this does and does not establish
|
||||
|
||||
**Establishes.** The user exists with the creation date `AGENTS.md` §7 records; it holds one
|
||||
inline policy and nothing else; the policy grants four actions on one bucket and
|
||||
one distribution; the four intended actions are `allowed`; five representative
|
||||
out-of-scope actions — including two against the client-database backup bucket —
|
||||
are denied; the backup bucket carries no resource-based policy that could grant
|
||||
around IAM; and one key exists that has never authenticated a request.
|
||||
|
||||
**Does not establish.** That the key works (it has never been used, and testing
|
||||
it would mean putting a real object in the bucket). That Gitea holds it —
|
||||
`AGENTS.md` §7 records **`GITEA ACTIONS SECRETS: UNSET`**, and Q23 is open on whether a runner
|
||||
exists at all. That the policy is sufficient for a deploy in practice: the
|
||||
`s3:AbortMultipartUpload` omission is covered by asset sizes rather than by a
|
||||
lifecycle rule, and `docs/06-deployment.md` records that with a revisit trigger.
|
||||
|
||||
**Not in scope of this file, and deliberately so.** The secret access key. It
|
||||
must never reach the repository — D3, and `AGENTS.md` §10 on the jointly administered Gitea
|
||||
instance, where an instance administrator can generally reach repository secrets
|
||||
or register a runner that receives them.
|
||||
Reference in New Issue
Block a user