Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/developer-guide/MCP-Headless-API.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/M

The stdio transport is the standard MCP local transport, exchanging newline delimited JSON-RPC messages. While it runs, application logging is redirected away from standard output so it can't corrupt the protocol stream. The stdio transport lives in the JavaSE port because it needs process standard input, which isn't available on every target.

=== Attaching to an application on a device

The socket transport binds the loopback interface, and it binds the loopback interface of whatever machine the application is running on. In the simulator that's your own, so an agent connects to `127.0.0.1` and there's nothing else to arrange. On a phone the port belongs to the phone, and reaching it needs a forward.

On Android, `adb forward tcp:8765 tcp:8765` maps a port on the development machine onto the same port inside the device, so an agent connects to `127.0.0.1:8765` as before. Remove it with `adb forward --remove tcp:8765` when the session ends. The `cn1:android-on-device-debugging` goal already installs and launches a debuggable build, and its `waitForAttach=false` option is the one to use when the point of the session is the agent rather than a breakpoint, since the application then boots straight into a drivable state instead of blocking for a debugger. On iOS the same setting isn't a convenience but a requirement, because `ios.onDeviceDebug.waitForAttach` defers the callback that boots the application until a debugger attaches -- so a starter in the application's own code never runs, and there is nothing listening on the port to reach.

The native iOS simulator shares the host's network stack, so the application's loopback port is the host's loopback port and no forwarding applies. A physical iPhone doesn't: the port sits on the device's own loopback, reachable only over the USB multiplexing channel, and `iproxy` from libimobiledevice is the usual relay. Codename One ships no goal for it, so an agent-driven session against a real iPhone depends on external tooling in a way the Android one doesn't.

None of this is a second security boundary. A forward is something you asked for, and the reason the server refuses a release build is that the loopback interface on a device is shared with every other application installed on it -- see <<development-builds-only,Development builds only>>.

[[development-builds-only]]
=== Development builds only

Expand Down
7 changes: 7 additions & 0 deletions docs/developer-guide/languagetool-accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -759,3 +759,10 @@ resizer
epoll
# A benchmark of one narrow operation, as against a whole application.
microbenchmark

# -----------------------------------------------------------------------------
# Attaching an agent to a build on a device (MCP-Headless-API.asciidoc).
# -----------------------------------------------------------------------------
# The open-source library whose iproxy tool relays a TCP port to an iPhone over
# USB. A project name, spelled lowercase by its authors.
libimobiledevice
70 changes: 60 additions & 10 deletions maven/cn1app-archetype/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,35 @@
</extensions>

<!--
Ship the Codename One authoring skill inside the archetype JAR under
archetype-resources/.claude/skills/codename-one/** so projects generated
via `mvn archetype:generate` get the same skill bundle the initializr
drops into start.codenameone.com output. The skill source of truth lives
under scripts/initializr/common/src/main/resources/skill/**; we copy it
in at build time rather than duplicating it in this module. The
generated project's archetype-post-generate.groovy deletes the .claude/
tree when the resolved javaVersion is 8, so only Java 17 projects end
up shipping the skill.
Ship the Codename One authoring skill inside the archetype JAR so projects
generated via `mvn archetype:generate` get the same three-part layout the
initializr drops into start.codenameone.com output, and the SAME layout
rather than merely the same content. An agent that only reads the generated files
finds the skill through whichever convention it knows:

AGENTS.md root pointer, vendor neutral
.agent-skills/codename-one/** the skill itself
.claude/skills/codename-one/SKILL.md thin stub, redirects to the above

Shipping the full skill under .claude/ instead (which this module did until
issue #5699) leaves a project generated from the archetype with no AGENTS.md
and no vendor-neutral copy, so Codex and anything else that does not know
Claude Code's directory layout never finds it.

All three sources of truth live under
scripts/initializr/common/src/main/resources/; we copy them in at build time
rather than duplicating them here, because a second copy is exactly how the
two generators drifted apart in the first place. The generated project's
archetype-post-generate.groovy deletes all three when the resolved
javaVersion is 8, so only Java 17 projects end up shipping the skill.
-->
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>${project.basedir}/../../scripts/initializr/common/src/main/resources/skill</directory>
<targetPath>archetype-resources/.claude/skills/codename-one</targetPath>
<targetPath>archetype-resources/.agent-skills/codename-one</targetPath>
<filtering>false</filtering>
</resource>
</resources>
Expand All @@ -78,5 +90,43 @@
</plugin>
</plugins>
</pluginManagement>

<plugins>
<!--
The root pointer and the Claude Code stub are both published under names
they are NOT stored under, which maven-resources cannot express, so ant
copies them. Same source directory as the <resource> entry above; the
point is that nothing here is authored twice.

The two renames have opposite reasons. The stub's published name is fixed
by Claude Code (SKILL.md). The pointer's source name avoids AGENTS.md,
because that is a name agents look for on their own: a file called that
inside this repository would be read as instructions for the Codename One
tree, and it describes a generated application instead.
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>stage-agent-skill-pointers</id>
<phase>process-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<copy file="${project.basedir}/../../scripts/initializr/common/src/main/resources/agent-skill-agents-md.md"
tofile="${project.build.outputDirectory}/archetype-resources/AGENTS.md"
overwrite="true"/>
<copy file="${project.basedir}/../../scripts/initializr/common/src/main/resources/agent-skill-claude-stub.md"
tofile="${project.build.outputDirectory}/archetype-resources/.claude/skills/codename-one/SKILL.md"
overwrite="true"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,13 @@ def pickJavaVersionFromCurrentJvm() {
/**
* Apply the Java-version-specific transforms that the initializr does on its
* server-rendered templates:
* - Java 17 keeps .claude/skills/codename-one/** (the Codename One authoring skill)
* - Java 8 strips .claude/ so older projects don't suddenly grow an AI-agent
* skill they never opted into.
* - Java 17 keeps the Codename One authoring skill in the same three-part
* layout the initializr generates: AGENTS.md (vendor-neutral root pointer),
* .agent-skills/codename-one/** (the skill), and .claude/skills/codename-one/
* SKILL.md (a thin stub redirecting to it)
* - Java 8 strips all three so older projects don't suddenly grow an AI-agent
* skill they never opted into. The skill's guidance is Java 17 anyway (var,
* records, text blocks, single-file source mode in tools/).
*
* The win/ module is the native win32 target and ships for every Java version
* (only the long-retired UWP module that previously lived under win/ used to be
Expand All @@ -134,9 +138,11 @@ def pickJavaVersionFromCurrentJvm() {
*/
def applyJavaVersionTransforms(rootDir, rootPom, resolvedJava) {
if (resolvedJava != "17") {
def claudeDir = new java.io.File(rootDir, ".claude")
if (claudeDir.exists()) {
deleteRecursively(claudeDir)
[".claude", ".agent-skills", "AGENTS.md"].each { name ->
def skillPath = new java.io.File(rootDir, name)
if (skillPath.exists()) {
deleteRecursively(skillPath)
}
}
}
setIntellijLanguageLevel(rootDir, resolvedJava)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,24 @@
</includes>
</fileSet>
<!--
.claude/skills/codename-one/** is staged into the archetype JAR at
build time from scripts/initializr/common/src/main/resources/skill/**
(see maven/cn1app-archetype/pom.xml). It always extracts; the post-
generate groovy deletes it when the resolved javaVersion is 8 so
only Java 17 projects ship the Codename One authoring skill.
The Codename One authoring skill, in the same three-part layout the
initializr generates: .agent-skills/codename-one/** is the skill itself,
.claude/skills/codename-one/SKILL.md is a thin stub that redirects to it,
and AGENTS.md (in the root fileSet below) is the vendor-neutral pointer.
All three are staged into the archetype JAR at build time from
scripts/initializr/common/src/main/resources/** (see
maven/cn1app-archetype/pom.xml). They always extract; the post-generate
groovy deletes all three when the resolved javaVersion is 8 so only Java 17
projects ship the skill.
filtered="false" — skill markdown has $foo placeholders that Velocity
would otherwise misinterpret.
-->
<fileSet encoding="UTF-8">
<directory>.agent-skills</directory>
<includes>
<include>**</include>
</includes>
</fileSet>
<fileSet encoding="UTF-8">
<directory>.claude</directory>
<includes>
Expand All @@ -82,6 +92,8 @@
<directory></directory>
<includes>
<include>.gitignore</include>
<!-- Root pointer to .agent-skills/; see the skill fileSets above. -->
<include>AGENTS.md</include>
<include>*.sh</include>
<include>*.bat</include>
<include>*.adoc</include>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,55 @@ private void applyCatalogPlistEntry(BuildRequest request,
}
}

/**
* Whether an on-device-debug proxy host is the loopback interface, and so needs
* no local-network declaration.
*
* Deliberately a SMALL allow-list rather than a parse: everything it does not
* recognise is treated as the local network, which is the answer that keeps a
* debugging session working. The whole 127/8 block counts, because loopback is
* 127.0.0.1 by convention and not by rule.
*/
static boolean isLoopbackDebugProxyHost(String host) {
if (host == null) {
return false;
}
String trimmed = host.trim();
// Brackets are how a literal IPv6 address is written in a host position.
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
trimmed = trimmed.substring(1, trimmed.length() - 1).trim();
}
if (trimmed.equalsIgnoreCase("localhost")
|| trimmed.equals("::1")
|| trimmed.equals("0:0:0:0:0:0:0:1")) {
return true;
}
if (!trimmed.startsWith("127.")) {
return false;
}
// "127.0.0.1" yes, "127.0.0.1.example.com" no -- a host name may begin with
// digits, and one that merely starts with the right four characters is not
// an address at all.
String[] parts = trimmed.split("\\.");
if (parts.length != 4) {
return false;
}
for (int i = 0; i < parts.length; i++) {
if (parts[i].length() == 0 || parts[i].length() > 3) {
return false;
}
for (int c = 0; c < parts[i].length(); c++) {
if (parts[i].charAt(c) < '0' || parts[i].charAt(c) > '9') {
return false;
}
}
if (Integer.parseInt(parts[i]) > 255) {
return false;
}
}
return true;
}

private int getDeploymentTargetInt(BuildRequest request) {
String target = getDeploymentTarget(request);
if (target.indexOf(".") > 0) {
Expand Down Expand Up @@ -16494,6 +16543,36 @@ public boolean accept(File file, String string) {
+ "<key>NSAllowsArbitraryLoads</key><true/>"
+ "</dict>";
}
// A PHYSICAL device reaches the proxy across the Wi-Fi it shares with
// the developer's machine, and since iOS 14 that is local-network
// access: consent-gated, and gated on a purpose string the app has to
// declare up front. Without one the app is terminated the moment
// cn1_debugger dials out -- before it can connect, so the session fails
// with the proxy still waiting and nothing on the device to explain it.
//
// Only for the LAN case. The native simulator shares the host's
// loopback, which is not the local network, and an unnecessary purpose
// string puts a prompt in front of a developer who never asked for one
// -- the same reason the nearby flags are kept apart from each other.
//
// Ambiguity resolves TOWARDS declaring it: a proxyHost that is not
// recognisably loopback may still be a LAN name rather than an address,
// and the costs are not symmetric. A spare purpose string costs one
// prompt in a build that is debug-only by construction; a missing one
// costs a debugging session that cannot start.
//
// Through applyCatalogPlistEntry rather than putArgument, for the reason
// the Matter block above states: the sweep that copies
// ios.NS*UsageDescription hints into privacyUsageDescriptions ran long
// before this line, the plist is rendered from that map, and a bare
// argument set here would never be read. It fills only a MISSING value,
// so a project that declared its own string keeps it.
if (!isLoopbackDebugProxyHost(proxyHost)) {
applyCatalogPlistEntry(request, new String[] {
"NSLocalNetworkUsageDescription",
"Connects to the Codename One debugging proxy on your computer. "
+ "This is a development build."});
}
}

// Export compliance: when the app uses com.codename1.security.* we
Expand Down
Loading
Loading