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
20 KiB
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
$ 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
$ 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
$ 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
$ 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.
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:
$ 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:
$ 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
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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.