-
Notifications
You must be signed in to change notification settings - Fork 2
Implement /health Endpoint #75 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
98b40a1
c1b5f70
bf4d977
148411e
9ac7b57
875d1ef
9289c7d
6bdb1ef
781e34a
511b5ee
aaeba6d
524f33c
1e7f375
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: Java CI with Maven | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ "main" ] | ||
| pull_request: | ||
| branches: [ "main" ] | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Get Java Version | ||
| run: | | ||
| JAVA_VERSION=$(mvn help:evaluate "-Dexpression=maven.compiler.release" -q -DforceStdout) | ||
| echo "JAVA_VERSION=$JAVA_VERSION" >> $GITHUB_ENV | ||
|
|
||
| - name: Set up JDK ${{ env.JAVA_VERSION }} | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: ${{ env.JAVA_VERSION }} | ||
| distribution: 'temurin' | ||
| cache: maven | ||
|
|
||
| - name: Compile with Maven | ||
| run: mvn -B compile --file pom.xml | ||
|
|
||
| - name: Test with Maven | ||
| run: mvn -B test --file pom.xml | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| name: Publish Docker Image to Github Packages on Release | ||
| on: | ||
| release: | ||
| types: | ||
| - published | ||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| packages: write | ||
| steps: | ||
| - uses: actions/checkout@v6.0.2 | ||
| - uses: docker/setup-qemu-action@v3.7.0 | ||
| - uses: docker/setup-buildx-action@v3.12.0 | ||
| - name: Log in to GHCR | ||
| uses: docker/login-action@v3.7.0 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Extract metadata | ||
| id: meta | ||
| uses: docker/metadata-action@v5.10.0 | ||
| with: | ||
| images: ghcr.io/ithsjava25/webserver | ||
| - name: Build and push | ||
| uses: docker/build-push-action@v6.18.0 | ||
| with: | ||
| context: . | ||
| push: true | ||
| platforms: linux/amd64, linux/arm64 | ||
| tags: ${{ steps.meta.outputs.tags }} | ||
| labels: ${{ steps.meta.outputs.labels }} | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,11 @@ | ||||||||||
| FROM maven:3-eclipse-temurin-25-alpine AS build | ||||||||||
| WORKDIR /build | ||||||||||
| COPY src/ src/ | ||||||||||
| COPY pom.xml pom.xml | ||||||||||
| RUN mvn compile | ||||||||||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The build stage only copies
🔧 Proposed fix FROM maven:3-eclipse-temurin-25-alpine AS build
WORKDIR /build
COPY src/ src/
+COPY www/ www/
COPY pom.xml pom.xml
-RUN mvn compile
+RUN mvn package -DskipTests
+RUN mvn dependency:copy-dependencies -DoutputDirectory=target/dependency
FROM eclipse-temurin:25-jre-alpine
+WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=build /build/target/classes/ /app/
+COPY --from=build /build/target/dependency/ /app/libs/
+COPY --from=build /build/www/ /app/www/
-ENTRYPOINT ["java", "-classpath", "/app", "org.example.App"]
+ENTRYPOINT ["java", "-classpath", "/app:/app/libs/*", "org.example.App"]
USER appuser🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| FROM eclipse-temurin:25-jre-alpine | ||||||||||
| RUN addgroup -S appgroup && adduser -S appuser -G appgroup | ||||||||||
| COPY --from=build /build/target/classes/ /app/ | ||||||||||
| ENTRYPOINT ["java", "-classpath", "/app", "org.example.App"] | ||||||||||
| USER appuser | ||||||||||
|
Comment on lines
+10
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
While Docker applies the final 🔧 Proposed fix COPY --from=build /build/target/classes/ /app/
+USER appuser
ENTRYPOINT ["java", "-classpath", "/app", "org.example.App"]
-USER appuser📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,13 +9,21 @@ | |
| <version>1.0-SNAPSHOT</version> | ||
|
|
||
| <properties> | ||
| <maven.compiler.release>23</maven.compiler.release> | ||
| <maven.compiler.release>25</maven.compiler.release> | ||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| <junit.jupiter.version>6.0.2</junit.jupiter.version> | ||
| <assertj.core.version>3.27.7</assertj.core.version> | ||
| <mockito.version>5.21.0</mockito.version> | ||
| <bucket4j.version>8.14.0</bucket4j.version> | ||
|
|
||
| </properties> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>com.bucket4j</groupId> | ||
| <artifactId>bucket4j_jdk17-core</artifactId> | ||
| <version>${bucket4j.version}</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter</artifactId> | ||
|
|
@@ -28,12 +36,36 @@ | |
| <version>${assertj.core.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-core</artifactId> | ||
| <version>${mockito.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.mockito</groupId> | ||
| <artifactId>mockito-junit-jupiter</artifactId> | ||
| <version>${mockito.version}</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.awaitility</groupId> | ||
| <artifactId>awaitility</artifactId> | ||
| <version>4.3.0</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
|
|
||
| <dependency> | ||
| <groupId>tools.jackson.core</groupId> | ||
| <artifactId>jackson-databind</artifactId> | ||
| <version>3.0.3</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>tools.jackson.dataformat</groupId> | ||
| <artifactId>jackson-dataformat-yaml</artifactId> | ||
| <version>3.0.3</version> | ||
| </dependency> | ||
|
|
||
| </dependencies> | ||
| <build> | ||
| <plugins> | ||
|
|
@@ -118,6 +150,39 @@ | |
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| <plugin> | ||
| <groupId>org.pitest</groupId> | ||
| <artifactId>pitest-maven</artifactId> | ||
| <version>1.22.0</version> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.pitest</groupId> | ||
| <artifactId>pitest-junit5-plugin</artifactId> | ||
| <version>1.2.2</version> | ||
| </dependency> | ||
| </dependencies> | ||
| </plugin> | ||
| <plugin> | ||
| <groupId>com.diffplug.spotless</groupId> | ||
| <artifactId>spotless-maven-plugin</artifactId> | ||
| <version>3.2.1</version> | ||
| <configuration> | ||
| <java> | ||
| <removeUnusedImports/> | ||
| <formatAnnotations/> | ||
| </java> | ||
| </configuration> | ||
| <executions> | ||
| <execution> | ||
| <phase>verify</phase> | ||
| <goals> | ||
| <goal> | ||
| check | ||
| </goal> | ||
| </goals> | ||
| </execution> | ||
| </executions> | ||
|
Comment on lines
+175
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Spotless execution is bound to the Either bind Spotless to the 🤖 Prompt for AI Agents |
||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package org.example; | ||
|
|
||
| import org.example.httpparser.HttpParser; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.Socket; | ||
|
|
||
| public class ConnectionHandler implements AutoCloseable { | ||
|
|
||
| Socket client; | ||
| String uri; | ||
|
|
||
| public ConnectionHandler(Socket client) { | ||
| this.client = client; | ||
| } | ||
|
|
||
| public void runConnectionHandler() throws IOException { | ||
| StaticFileHandler sfh = new StaticFileHandler(); | ||
| HttpParser parser = new HttpParser(); | ||
| parser.setReader(client.getInputStream()); | ||
| parser.parseRequest(); | ||
| parser.parseHttp(); | ||
|
|
||
| // --- DIN ÄNDRING FÖR ISSUE #75 BÖRJAR HÄR --- | ||
| String requestedUri = parser.getUri(); | ||
| if (requestedUri.equals("/health")) { | ||
| String responseBody = "{\"status\": \"ok\"}"; | ||
| String header = "HTTP/1.1 200 OK\r\n" + | ||
| "Content-Type: application/json\r\n" + | ||
| "Content-Length: " + responseBody.length() + "\r\n" + | ||
| "\r\n"; | ||
|
|
||
| client.getOutputStream().write(header.getBytes()); | ||
| client.getOutputStream().write(responseBody.getBytes()); | ||
| client.getOutputStream().flush(); | ||
| return; // Avslutar här så vi inte letar efter filer i onödan | ||
| } | ||
| // --- DIN ÄNDRING SLUTAR HÄR --- | ||
|
Comment on lines
+24
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove developer scaffolding comments before merge. The Swedish 🤖 Prompt for AI Agents |
||
|
|
||
| resolveTargetFile(requestedUri); | ||
| sfh.sendGetRequest(client.getOutputStream(), uri); | ||
| } | ||
|
|
||
| private void resolveTargetFile(String uri) { | ||
| if (uri.matches("/$")) { //matches(/) | ||
| this.uri = "index.html"; | ||
| } else if (uri.matches("^(?!.*\\.html$).*$")) { | ||
| this.uri = uri.concat(".html"); | ||
| } else { | ||
| this.uri = uri; | ||
| } | ||
|
Comment on lines
+44
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Locate StaticFileHandler and inspect how it opens/reads the file
fd -e java -g "StaticFileHandler.java" --exec cat {}Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template Length of output: 1079 Fix path traversal vulnerability in The vulnerability claim is confirmed. Resolve this by normalizing the path in Path normalizedPath = Paths.get(WEB_ROOT).resolve(uri).normalize();
if (!normalizedPath.startsWith(Paths.get(WEB_ROOT))) {
throw new IOException("Access denied: path traversal detected");
}
File file = normalizedPath.toFile();🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Override | ||
| public void close() throws Exception { | ||
| client.close(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||||||||||||||||
| package org.example; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import org.example.http.HttpResponseBuilder; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import java.io.File; | ||||||||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||||||||
| import java.io.OutputStream; | ||||||||||||||||||||||||||||
| import java.io.PrintWriter; | ||||||||||||||||||||||||||||
| import java.nio.file.Files; | ||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| public class StaticFileHandler { | ||||||||||||||||||||||||||||
| private static final String WEB_ROOT = "www"; | ||||||||||||||||||||||||||||
| private byte[] fileBytes; | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the same Move ♻️ Proposed fix public class StaticFileHandler {
private static final String WEB_ROOT = "www";
- private byte[] fileBytes;
- private void handleGetRequest(String uri) throws IOException {
- File file = new File(WEB_ROOT, uri);
- fileBytes = Files.readAllBytes(file.toPath());
- }
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
- handleGetRequest(uri);
+ File file = new File(WEB_ROOT, uri);
+ byte[] fileBytes = Files.readAllBytes(file.toPath());
HttpResponseBuilder response = new HttpResponseBuilder();
response.setHeaders(Map.of("Content-Type", "text/html; charset=utf-8"));
response.setBody(fileBytes);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| public StaticFileHandler(){} | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| private void handleGetRequest(String uri) throws IOException { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| File file = new File(WEB_ROOT, uri); | ||||||||||||||||||||||||||||
| fileBytes = Files.readAllBytes(file.toPath()); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Path traversal vulnerability — user-controlled URI is not sanitised before file access.
Always compare the canonical path of the resolved file against the canonical path of 🔒 Proposed fix private void handleGetRequest(String uri) throws IOException {
- File file = new File(WEB_ROOT, uri);
+ File root = new File(WEB_ROOT).getCanonicalFile();
+ File file = new File(root, uri).getCanonicalFile();
+ if (!file.getPath().startsWith(root.getPath() + File.separator)
+ && !file.getPath().equals(root.getPath())) {
+ throw new IOException("Forbidden path: " + uri);
+ }
fileBytes = Files.readAllBytes(file.toPath());
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| public void sendGetRequest(OutputStream outputStream, String uri) throws IOException{ | ||||||||||||||||||||||||||||
| handleGetRequest(uri); | ||||||||||||||||||||||||||||
| HttpResponseBuilder response = new HttpResponseBuilder(); | ||||||||||||||||||||||||||||
| response.setHeaders(Map.of("Content-Type", "text/html; charset=utf-8")); | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Content-Type is hardcoded to Every file — CSS, JavaScript, JSON, images — will be served as 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| response.setBody(fileBytes); | ||||||||||||||||||||||||||||
| PrintWriter writer = new PrintWriter(outputStream, true); | ||||||||||||||||||||||||||||
| writer.println(response.build()); | ||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Two distinct bugs here:
Write the response directly as UTF-8 bytes instead: 🐛 Proposed fix- PrintWriter writer = new PrintWriter(outputStream, true);
- writer.println(response.build());
+ outputStream.write(response.build().getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ outputStream.flush();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package org.example; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.ServerSocket; | ||
| import java.net.Socket; | ||
|
|
||
| public class TcpServer { | ||
|
|
||
| private final int port; | ||
|
|
||
| public TcpServer(int port) { | ||
| this.port = port; | ||
| } | ||
|
|
||
| public void start() { | ||
| System.out.println("Starting TCP server on port " + port); | ||
|
|
||
| try (ServerSocket serverSocket = new ServerSocket(port)) { | ||
| while (true) { | ||
| Socket clientSocket = serverSocket.accept(); // block | ||
| System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress()); | ||
| Thread.ofVirtual().start(() -> handleClient(clientSocket)); | ||
| } | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("Failed to start TCP server", e); | ||
| } | ||
| } | ||
|
|
||
| private void handleClient(Socket client) { | ||
| try (ConnectionHandler connectionHandler = new ConnectionHandler(client)) { | ||
| connectionHandler.runConnectionHandler(); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Error handling client connection " + e); | ||
| } | ||
|
Comment on lines
+22
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two related exception-handling problems.
🔧 Proposed fix- Thread.ofVirtual().start(() -> handleClient(clientSocket));
+ Thread.ofVirtual()
+ .uncaughtExceptionHandler((t, ex) ->
+ logger.log(Level.SEVERE, "Unhandled error in client thread", ex))
+ .start(() -> handleClient(clientSocket));- throw new RuntimeException("Error handling client connection " + e);
+ throw new RuntimeException("Error handling client connection", e);🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package org.example.http; | ||
| // | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class HttpResponseBuilder { | ||
|
|
||
| private static final String PROTOCOL = "HTTP/1.1"; | ||
| private int statusCode = 200; | ||
| private String body = ""; | ||
| private byte[] bytebody; | ||
| private Map<String, String> headers = new LinkedHashMap<>(); | ||
|
|
||
| private static final String CRLF = "\r\n"; | ||
|
|
||
|
|
||
| public void setStatusCode(int statusCode) { | ||
| this.statusCode = statusCode; | ||
| } | ||
| public void setBody(String body) { | ||
| this.body = body != null ? body : ""; | ||
| } | ||
|
|
||
| public void setBody(byte[] body) { | ||
| this.bytebody = body; | ||
| } | ||
| public void setHeaders(Map<String, String> headers) { | ||
| this.headers = new LinkedHashMap<>(headers); | ||
| } | ||
|
|
||
| private static final Map<Integer, String> REASON_PHRASES = Map.of( | ||
| 200, "OK", | ||
| 201, "Created", | ||
| 400, "Bad Request", | ||
| 404, "Not Found", | ||
| 500, "Internal Server Error"); | ||
| public String build(){ | ||
| StringBuilder sb = new StringBuilder(); | ||
| int contentLength; | ||
| if(body.isEmpty() && bytebody != null){ | ||
| contentLength = bytebody.length; | ||
| setBody(new String(bytebody, StandardCharsets.UTF_8)); | ||
| }else{ | ||
| contentLength = body.getBytes(StandardCharsets.UTF_8).length; | ||
| } | ||
|
|
||
|
|
||
| String reason = REASON_PHRASES.getOrDefault(statusCode, "OK"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unknown status codes fall back to
🤖 Prompt for AI Agents |
||
| sb.append(PROTOCOL).append(" ").append(statusCode).append(" ").append(reason).append(CRLF); | ||
| headers.forEach((k,v) -> sb.append(k).append(": ").append(v).append(CRLF)); | ||
| sb.append("Content-Length: ") | ||
| .append(contentLength); | ||
| sb.append(CRLF); | ||
| sb.append("Connection: close").append(CRLF); | ||
| sb.append(CRLF); | ||
| sb.append(body); | ||
| return sb.toString(); | ||
|
|
||
| } | ||
|
Comment on lines
+38
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 43 calls
Refactor ♻️ Proposed fix public String build(){
StringBuilder sb = new StringBuilder();
int contentLength;
+ String bodyToWrite;
if(body.isEmpty() && bytebody != null){
contentLength = bytebody.length;
- setBody(new String(bytebody, StandardCharsets.UTF_8));
+ bodyToWrite = new String(bytebody, StandardCharsets.UTF_8);
}else{
contentLength = body.getBytes(StandardCharsets.UTF_8).length;
+ bodyToWrite = body;
}
String reason = REASON_PHRASES.getOrDefault(statusCode, "OK");
sb.append(PROTOCOL).append(" ").append(statusCode).append(" ").append(reason).append(CRLF);
headers.forEach((k,v) -> sb.append(k).append(": ").append(v).append(CRLF));
sb.append("Content-Length: ").append(contentLength);
sb.append(CRLF);
sb.append("Connection: close").append(CRLF);
sb.append(CRLF);
- sb.append(body);
+ sb.append(bodyToWrite);
return sb.toString();
}🤖 Prompt for AI Agents |
||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mvn -B teststops before theverifyphase — Spotless and Failsafe never execute in CI.The
spotless:checkgoal is bound toverifyinpom.xml(line 177) and integration tests usemaven-failsafe-pluginwhich also runs inverify. Neither will fire withmvn test. Consider updating tomvn -B verify(or at a minimummvn -B test spotless:check) to enforce formatting and run integration tests in CI.🤖 Prompt for AI Agents