Skip to content

Commit 128774d

Browse files
committed
fix release validation and imaging deployment
1 parent 0d05000 commit 128774d

8 files changed

Lines changed: 201 additions & 19 deletions

File tree

‎compose.dev.yml‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ services:
108108
keycloak:
109109
condition: service_started
110110
healthcheck:
111-
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
111+
test: ["CMD", "node", "-e", "require('node:http').get('http://127.0.0.1:4000/health',r=>{r.resume();process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"]
112112
interval: 5s
113113
timeout: 4s
114114
retries: 30
@@ -145,4 +145,3 @@ volumes:
145145
redis-dev-data:
146146
keycloak-dev-data:
147147
imaging-dev-data:
148-
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# ModelForge v1.3.0 release validation
2+
3+
Date: 2026-08-31
4+
5+
## Result
6+
7+
The installed Windows release and the repository build passed the automated,
8+
integration, container, and visual checks listed below. The validation recording
9+
contains synthetic, PHI-free data and captures only the ModelForge application
10+
window.
11+
12+
[Watch the compact release proof](assets/modelforge-v1.3.0-release-proof.mp4)
13+
14+
## Installed release walkthrough
15+
16+
The 3-minute, 655 KB H.264 recording demonstrates:
17+
18+
- local-only chat and the new-chat workflow;
19+
- Runtime Manager detection and live telemetry for an NVIDIA RTX 4060;
20+
- balanced, performance, efficient, and manual resource profiles;
21+
- Download Center limits and concurrent-download controls;
22+
- audit, privacy, encryption, and audit-backend status;
23+
- a synthetic patient case and its inclusion controls;
24+
- Evidence Library and Knowledge Graph navigation.
25+
26+
No AWS credential, AWS account identifier, prompt, patient identifier, or other
27+
secret is visible in the recording or this report.
28+
29+
## Automated evidence
30+
31+
| Area | Result |
32+
| --- | --- |
33+
| Desktop application unit tests | 1,018 passed, 1 skipped across 78 files |
34+
| Server default test mode | 650 passed, 84 skipped across 55 files |
35+
| Server with isolated PostgreSQL and Redis | 731 passed, 3 opt-in live-adapter tests skipped across 55 files |
36+
| Frontend tests | 101 passed |
37+
| Admin console tests | 61 passed |
38+
| MasterVault tests | 14 passed |
39+
| Rust native tests | 80 passed |
40+
| Playwright desktop E2E | 25 passed |
41+
| Imaging CDK regression tests | 3 passed |
42+
43+
The Playwright suite covers approvals, audit persistence and backend migration,
44+
backup/restore, signed central policy handling, cancellation, Download Center,
45+
llama.cpp chat and agent tool gating, medication safety, onboarding, patient-case
46+
backend configuration, response contracts, settings persistence, and
47+
single-instance behavior.
48+
49+
Production builds/typechecks passed for the desktop application, server,
50+
frontend, admin console, shared contracts, MasterVault, Rust native component,
51+
E2E package, and imaging CDK. Frontend and admin lint also passed.
52+
53+
The development container stack was rebuilt and verified healthy. PostgreSQL,
54+
Redis, the API, Keycloak, and the admin console all started successfully; the API
55+
health endpoint returned `{"status":"ok"}` and the admin console returned HTTP
56+
200.
57+
58+
## Defects corrected during validation
59+
60+
1. The server container could load a stale copied `@modelforge/contracts`
61+
package and fail during startup. The image now links the workspace contracts
62+
package after dependency installation.
63+
2. The API health check did not reliably consume the response or terminate from
64+
the HTTP status. It now exits deterministically after consuming the response.
65+
3. The PostgreSQL migration-audit test ordered same-timestamp events only by
66+
`created_at`. It now asserts the audit ledger's monotonic numeric sequence.
67+
4. The imaging CDK stack contained a redundant exact-distribution KMS grant that
68+
created a CloudFormation dependency cycle. The stack now relies on the CDK
69+
OAC-generated, account-scoped distribution grant.
70+
5. The dedicated CloudFront access-log bucket used bucket-owner-enforced object
71+
ownership, which is incompatible with CloudFront legacy standard logging. It
72+
now enables the required object-writer ownership while retaining public-access
73+
blocking. The PHI imaging bucket remains bucket-owner-enforced.
74+
6. CloudFormation descriptions used unsupported punctuation. They now use the
75+
supported ASCII character set.
76+
77+
Regression tests protect the CloudFront log-bucket ownership, KMS grant shape,
78+
and CloudFormation description character set.
79+
80+
## External deployment status
81+
82+
The imaging stack synthesized successfully after the fixes. Deployment reached
83+
AWS and exposed the CloudFront logging and dependency-cycle defects above, but a
84+
subsequent deploy is blocked before resource mutation because the account's CDK
85+
bootstrap toolkit is version 28 and the current deployment requires version 30.
86+
Upgrading the bootstrap stack changes account-level deployment IAM and therefore
87+
requires explicit operator approval. No credential value was read or printed.
88+
89+
Production Compose configuration additionally requires deployment-specific
90+
`OIDC_ISSUER` configuration; no value was invented for validation.
91+
92+
## Scope boundary
93+
94+
This run verifies all repository-owned automated suites and the installed
95+
workflows listed above. Provider-dependent live imaging adapters, real clinical
96+
data flows, real external model/provider credentials, and the final AWS resource
97+
deployment require their respective controlled environments and are not claimed
98+
as exercised here.
655 KB
Binary file not shown.

‎infra/imaging-cdk/lib/imaging-stack.ts‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class ImagingStack extends Stack {
4343
// once the bucket and distribution exist, scoped exactly to the
4444
// two principals the table calls for — nothing broader.
4545
const cmk = new kms.Key(this, "ImagingCmk", {
46-
description: "CMK for ModelForge imaging object storage (DICOM instances) — see docs/IMAGING.md",
46+
description: "CMK for ModelForge imaging object storage (DICOM instances) - see docs/IMAGING.md",
4747
enableKeyRotation: true,
4848
removalPolicy: RemovalPolicy.RETAIN,
4949
});
@@ -56,7 +56,11 @@ export class ImagingStack extends Stack {
5656
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
5757
encryption: s3.BucketEncryption.S3_MANAGED,
5858
enforceSSL: true,
59-
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
59+
// CloudFront standard logging (legacy) requires ACLs and CDK
60+
// explicitly requires OBJECT_WRITER for a custom log bucket.
61+
// This bucket contains access logs only; the PHI-bearing imaging
62+
// bucket below remains BUCKET_OWNER_ENFORCED with ACLs disabled.
63+
objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
6064
lifecycleRules: [{ expiration: Duration.days(365) }],
6165
removalPolicy: RemovalPolicy.RETAIN,
6266
});
@@ -86,7 +90,7 @@ export class ImagingStack extends Stack {
8690
});
8791

8892
const distribution = new cloudfront.Distribution(this, "ImagingDistribution", {
89-
comment: "ModelForge imaging (DICOM) content delivery — see docs/IMAGING.md",
93+
comment: "ModelForge imaging (DICOM) content delivery - see docs/IMAGING.md",
9094
defaultBehavior: {
9195
// withOriginAccessControl(): OAC, not the legacy OAI this
9296
// table explicitly rules out — CDK wires the bucket policy
@@ -99,20 +103,14 @@ export class ImagingStack extends Stack {
99103
},
100104
logBucket: accessLogBucket,
101105
logFilePrefix: "cloudfront/",
102-
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
103106
});
104107

105-
// KMS grants: exactly the two principals the table names, nothing
106-
// broader. The CloudFront OAC service principal is scoped to this
107-
// one distribution's ARN — not "any CloudFront distribution in this
108-
// account" — via aws:SourceArn, the standard pattern for an
109-
// OAC-fronted, KMS-encrypted S3 origin.
110-
cmk.grant(
111-
new iam.ServicePrincipal("cloudfront.amazonaws.com", {
112-
conditions: { StringEquals: { "aws:SourceArn": `arn:${this.partition}:cloudfront::${this.account}:distribution/${distribution.distributionId}` } },
113-
}),
114-
"kms:Decrypt"
115-
);
108+
// S3BucketOrigin.withOriginAccessControl() adds the CloudFront KMS
109+
// decrypt statement itself. Its source ARN is limited to this AWS
110+
// account's distributions but intentionally uses a distribution-ID
111+
// wildcard: making the key policy depend on this distribution while
112+
// the distribution also depends on the KMS-backed bucket creates a
113+
// CloudFormation cycle and makes a first deployment impossible.
116114

117115
// Server task role: s3:PutObject/GetObject/DeleteObject on the
118116
// bucket prefix, plus the KMS grants above. Explicitly NOT
@@ -121,7 +119,7 @@ export class ImagingStack extends Stack {
121119
// identity actually runs server/ (see README.md).
122120
const serverTaskRole = new iam.Role(this, "ImagingServerTaskRole", {
123121
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
124-
description: "Least-privilege role for the server/ process's S3ImagingObjectStore — attach to whatever compute actually runs it (see infra/imaging-cdk/README.md).",
122+
description: "Least-privilege role for server S3 imaging access; attach to the compute running server (see infra/imaging-cdk/README.md).",
125123
});
126124
bucket.grantPut(serverTaskRole);
127125
bucket.grantRead(serverTaskRole);

‎infra/imaging-cdk/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"build": "tsc",
99
"clean": "rm -rf dist cdk.out",
10+
"test": "tsx --test test/*.test.ts",
1011
"typecheck": "tsc --noEmit",
1112
"synth": "cdk synth",
1213
"diff": "cdk diff",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import assert from "node:assert/strict";
2+
import { generateKeyPairSync } from "node:crypto";
3+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
import test from "node:test";
7+
import { App } from "aws-cdk-lib";
8+
import { Template } from "aws-cdk-lib/assertions";
9+
import { ImagingStack } from "../lib/imaging-stack.js";
10+
11+
function synthesize(): Record<string, any> {
12+
const directory = mkdtempSync(join(tmpdir(), "modelforge-imaging-cdk-"));
13+
try {
14+
const { publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
15+
const publicKeyPath = join(directory, "signing-public-key.pem");
16+
writeFileSync(publicKeyPath, publicKey.export({ type: "spki", format: "pem" }));
17+
const app = new App();
18+
const stack = new ImagingStack(app, "TestImagingStack", {
19+
env: { account: "111111111111", region: "eu-central-1" },
20+
signingPublicKeyPath: publicKeyPath,
21+
bucketNamePrefix: "modelforge-imaging-test",
22+
});
23+
return Template.fromStack(stack).toJSON();
24+
} finally {
25+
rmSync(directory, { recursive: true, force: true });
26+
}
27+
}
28+
29+
test("keeps ACLs enabled only on the dedicated CloudFront log bucket", () => {
30+
const template = synthesize();
31+
const resources = template.Resources as Record<string, any>;
32+
const imagingBucket = Object.values(resources).find((resource: any) =>
33+
resource.Type === "AWS::S3::Bucket" && resource.Properties?.LoggingConfiguration
34+
) as any;
35+
assert.ok(imagingBucket, "expected the imaging bucket with server-access logging");
36+
37+
const logBucketId = imagingBucket.Properties.LoggingConfiguration.DestinationBucketName.Ref;
38+
const logBucket = resources[logBucketId];
39+
assert.deepEqual(logBucket.Properties.OwnershipControls.Rules, [{ ObjectOwnership: "ObjectWriter" }]);
40+
assert.deepEqual(imagingBucket.Properties.OwnershipControls.Rules, [{ ObjectOwnership: "BucketOwnerEnforced" }]);
41+
assert.deepEqual(logBucket.Properties.PublicAccessBlockConfiguration, {
42+
BlockPublicAcls: true,
43+
BlockPublicPolicy: true,
44+
IgnorePublicAcls: true,
45+
RestrictPublicBuckets: true,
46+
});
47+
});
48+
49+
test("uses the OAC-generated account-scoped KMS grant without a distribution dependency cycle", () => {
50+
const template = synthesize();
51+
const key = Object.values(template.Resources as Record<string, any>).find((resource: any) => resource.Type === "AWS::KMS::Key") as any;
52+
assert.ok(key, "expected a customer-managed KMS key");
53+
const cloudFrontStatements = key.Properties.KeyPolicy.Statement.filter(
54+
(statement: any) => statement.Principal?.Service === "cloudfront.amazonaws.com"
55+
);
56+
assert.equal(cloudFrontStatements.length, 1);
57+
assert.deepEqual(
58+
cloudFrontStatements[0].Condition.ArnLike["AWS:SourceArn"],
59+
{
60+
"Fn::Join": [
61+
"",
62+
[
63+
"arn:",
64+
{ Ref: "AWS::Partition" },
65+
":cloudfront::",
66+
{ Ref: "AWS::AccountId" },
67+
":distribution/*",
68+
],
69+
],
70+
}
71+
);
72+
});
73+
74+
test("keeps CloudFormation descriptions within the supported character set", () => {
75+
const template = synthesize();
76+
for (const resource of Object.values(template.Resources as Record<string, any>) as any[]) {
77+
const description = resource.Properties?.Description;
78+
if (description === undefined) continue;
79+
assert.match(description, /^[\u0009\u000A\u000D\u0020-\u007E\u00A1-\u00FF]*$/);
80+
}
81+
});

‎server/Dockerfile‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ COPY packages/contracts/package.json packages/contracts/tsconfig.json ./packages
55
COPY packages/contracts/src ./packages/contracts/src
66
COPY server/package.json server/package-lock.json ./server/
77
COPY server/vendor ./server/vendor
8+
# npm copies file: dependencies inside container builds. Point the server at
9+
# the workspace package so the development-stage contract build below is the
10+
# exact code imported by tsx, not a stale copy captured before that build ran.
811
RUN --mount=type=cache,target=/root/.npm \
912
npm --prefix server ci \
13+
&& rm -rf server/node_modules/@modelforge/contracts \
14+
&& ln -s ../../../packages/contracts server/node_modules/@modelforge/contracts \
1015
&& ln -s ../../server/node_modules packages/contracts/node_modules
1116

1217
FROM dependencies AS development

‎server/src/store/postgres-case-migration-store.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe.skipIf(!DATABASE_URL)("PostgresCaseMigrationStore (integration — requ
138138
await repo.rollback(session.id, ACTOR);
139139

140140
const rows = await pool.query(
141-
"SELECT action FROM audit_log WHERE organization_id = $1 AND target_id = $2 ORDER BY created_at",
141+
"SELECT action FROM audit_log WHERE organization_id = $1 AND target_id = $2 ORDER BY sequence::bigint",
142142
[orgId, session.id]
143143
);
144144
expect(rows.rows.map((r: { action: string }) => r.action)).toEqual([

0 commit comments

Comments
 (0)