Skip to content

Commit 8a90acc

Browse files
authored
Release 0.0.22: mobile gateways and product fixes (#31)
* Release 0.0.22: mobile gateways and product fixes * Test the packaged mobile gateway shell --------- Co-authored-by: Joseph Yaksich <gitcommit90@users.noreply.github.com>
1 parent 52c3b42 commit 8a90acc

35 files changed

Lines changed: 1066 additions & 242 deletions

CHANGELOG.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.0.22] - 2026-07-28
11+
12+
### Added
13+
14+
- Presentations now define a visible, locked printable boundary on every
15+
slide, defaulting to 1500 × 1000 with configurable dimensions. The
16+
Excalidraw menu exports the whole bounded deck as one real multi-page PDF and
17+
excludes material outside each printable page.
18+
- Android and iOS now package only a minimal HTTPS instance connection and
19+
recovery shell. After selection they load the chosen instance's live
20+
frontend directly, preserve native secure sessions, notifications, links,
21+
attachments, microphone, keyboard, and safe-area behavior, and confine the
22+
native bridge to an exact scheme/host/effective-port match.
23+
24+
### Changed
25+
26+
- Fresh browser profiles start in light mode, while retaining the existing
27+
user-controlled dark-mode switch.
28+
- The Cowork section tabs are centered at normal widths.
29+
- Routes now presents a spacious bottom-to-top request flow through **1Helm
30+
Router** into a stable arc of ChatGPT, Claude, Antigravity, xAI, OpenRouter,
31+
NVIDIA, Cloudflare, and GLM. Dotted live paths and the request ledger keep
32+
requested model policy separate from the routed provider or fallback.
33+
- Structured question choices now show an accessible pressed state with a
34+
strong ring, fill, inset accent, and checked indicator in both themes.
35+
36+
### Fixed
37+
38+
- **Log Out** is now available in the profile menu. Native profiles also retain
39+
their separate **Disconnect** action for erasing the selected instance and
40+
secure session.
41+
- Every composer now displays and submits the authoritative effective model
42+
policy using thread → channel → personal → workspace → agent precedence.
43+
Personal overrides are labeled and can be cleared with **Use workspace
44+
default**; stale submissions are rejected before admission, and queued turns
45+
retain the model and provider snapshot they were admitted with.
46+
- Silent thread audits use their own `system-*` routing identity and the
47+
Captain-scoped provider fabric, so concurrent user requests cannot be
48+
mislabeled as maintenance. Router fallback remains a provider outcome rather
49+
than appearing to change the requested model mid-turn.
50+
- HTTP browser instances now use `ws:` while HTTPS and native instances use
51+
`wss:`. Fast Cowork collaboration sync, detached Files test rows, and legacy
52+
presentation element ordering are also handled deterministically.
53+
54+
### Tests
55+
56+
- Native integration coverage proves visible personal-policy execution,
57+
immediate restoration of the workspace default, stale-policy rejection,
58+
durable admission metadata, and queued-model immutability across a later
59+
preference change.
60+
- Browser coverage verifies profile Log Out, first-profile light mode, centered
61+
Cowork tabs, selected answers in both themes, the literal eight-provider
62+
Routes geometry, the locked presentation boundary, configurable page size,
63+
and a parsed two-page 1600 × 900 PDF export.
64+
- Mobile contracts prove only gateway assets are packaged, HTTPS origins are
65+
normalized, native sessions stay secure, and Android/iOS navigation cannot
66+
broaden bridge access through an origin-prefix attack.
67+
1068
## [0.0.21] - 2026-07-27
1169

1270
### Fixed
@@ -642,7 +700,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
642700
notarization, stapled tickets, Gatekeeper verification, persistent
643701
Application Support, and isolated Apple container machines.
644702

645-
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.21...HEAD
703+
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.22...HEAD
704+
[0.0.22]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.22
646705
[0.0.21]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.21
647706
[0.0.20]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.20
648707
[0.0.19]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.19

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,11 @@ acceptance from the same source commit.
143143

144144
The mobile apps are thin, native gateways to a 1Helm you already run. Install
145145
and finish setup on a supported Mac, Windows, or Linux host first, give that
146-
host an HTTPS address, then enter the address, username, and password in the
147-
mobile app. The password is used only for sign-in and is never retained; the
148-
resulting session is stored in the iOS Keychain or encrypted with a key held by
149-
Android Keystore.
146+
host an HTTPS address, then select that address in the app. The app loads the
147+
selected instance's live frontend directly, so browsers, iPhone, iPad, and
148+
Android always use the same current interface. Sign-in happens on that live
149+
frontend; the password is never retained and the resulting session is stored
150+
in the iOS Keychain or encrypted with a key held by Android Keystore.
150151

151152
- iPhone and iPad use the App Store build under the stable bundle identifier
152153
`com.gitcommit90.onehelm.mobile`.
@@ -155,9 +156,11 @@ Android Keystore.
155156
you used to open the file. Verify the release SHA-256 and install it; future
156157
releases signed by the same permanent 1Helm certificate install in place.
157158
- The native clients require HTTPS, do not contain or initialize the 1Helm
158-
server, and do not retain host data or provider credentials beyond the
159-
selected server address and secure session token. Use **Disconnect** in the
160-
profile menu to erase both from the device.
159+
server or a frozen copy of its product frontend, and do not retain host data
160+
or provider credentials beyond the selected server address and secure
161+
session token. Native bridge access is restricted to that exact scheme,
162+
host, and port. Use **Disconnect** in the profile menu to erase both from the
163+
device.
161164
- iPhone notifications are opt-in under **Settings → Notifications**. Device
162165
registration belongs to the signed-in account, per-channel mute still wins,
163166
and tapping an update opens its channel or thread. iOS camera and photo
@@ -252,6 +255,9 @@ and an audit trail. A prompt saying “use this service” is not a connector.
252255
`/workspace/presentations`, with one consistent file rail, live collaborative
253256
text editors, embedded whiteboard and slide canvases, and an optional
254257
channel-agent panel that starts an ordinary thread with the open file path.
258+
- Presentations define a visible, locked printable area (1500 × 1000 by
259+
default), allow custom page dimensions, and export the complete bounded slide
260+
deck as one multi-page PDF while excluding work outside each page.
255261
- Quick Note in the top bar for capturing a Markdown note into
256262
`/workspace/notes` without leaving or repositioning the current channel view.
257263
- Durable files, threads, curated memory, Mnemosyne long-term recall,
@@ -301,7 +307,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to
301307
| `PORT` | `8123` | HTTP/WebSocket control-plane port. |
302308
| `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and narrow workspace mirrors. |
303309
| `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `lxc` on Linux, `wsl` on Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. |
304-
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.21` | Versioned channel-machine image contract. |
310+
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.22` | Versioned channel-machine image contract. |
305311

306312
### Agent-first JSON CLI
307313

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.gitcommit90.onehelm.mobile;
2+
3+
import android.content.Context;
4+
import android.content.Intent;
5+
import android.net.Uri;
6+
import com.getcapacitor.JSObject;
7+
import com.getcapacitor.Plugin;
8+
import com.getcapacitor.PluginCall;
9+
import com.getcapacitor.PluginMethod;
10+
import com.getcapacitor.annotation.CapacitorPlugin;
11+
12+
@CapacitorPlugin(name = "InstanceGateway")
13+
public class InstanceGatewayPlugin extends Plugin {
14+
static final String PREFS = "onehelm_instance_gateway";
15+
static final String SERVER = "selected_https_origin";
16+
17+
static String validOrigin(String raw) {
18+
if (raw == null) return null;
19+
Uri uri = Uri.parse(raw.trim());
20+
if (!"https".equalsIgnoreCase(uri.getScheme()) || uri.getHost() == null || uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null) return null;
21+
String path = uri.getPath();
22+
if (path != null && !path.isEmpty() && !"/".equals(path)) return null;
23+
return new Uri.Builder().scheme("https").encodedAuthority(uri.getEncodedAuthority()).build().toString();
24+
}
25+
26+
private static int effectivePort(Uri uri) {
27+
if (uri.getPort() >= 0) return uri.getPort();
28+
return "https".equalsIgnoreCase(uri.getScheme()) ? 443 : "http".equalsIgnoreCase(uri.getScheme()) ? 80 : -1;
29+
}
30+
31+
private static boolean sameOrigin(Uri left, Uri right) {
32+
return left != null && right != null && left.getScheme() != null && right.getScheme() != null
33+
&& left.getHost() != null && right.getHost() != null
34+
&& left.getScheme().equalsIgnoreCase(right.getScheme())
35+
&& left.getHost().equalsIgnoreCase(right.getHost())
36+
&& effectivePort(left) == effectivePort(right);
37+
}
38+
39+
@Override
40+
public Boolean shouldOverrideLoad(Uri url) {
41+
String scheme = url == null ? "" : String.valueOf(url.getScheme()).toLowerCase();
42+
if ("data".equals(scheme) || "blob".equals(scheme)) return false;
43+
if (!"http".equals(scheme) && !"https".equals(scheme)) return null;
44+
String selected = validOrigin(getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(SERVER, null));
45+
// The bundled connection/error shell and the exact selected origin are
46+
// the only HTTP(S) documents allowed inside this bridge. External
47+
// links are opened by the shared frontend before navigation begins.
48+
return !(sameOrigin(url, Uri.parse("https://localhost")) || (selected != null && sameOrigin(url, Uri.parse(selected))));
49+
}
50+
51+
@PluginMethod
52+
public void getServer(PluginCall call) {
53+
String origin = validOrigin(getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(SERVER, null));
54+
JSObject result = new JSObject(); result.put("origin", origin == null ? "" : origin); call.resolve(result);
55+
}
56+
57+
@PluginMethod
58+
public void selectServer(PluginCall call) {
59+
String origin = validOrigin(call.getString("origin"));
60+
if (origin == null) { call.reject("Choose a valid HTTPS root address."); return; }
61+
getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(SERVER, origin).apply();
62+
JSObject result = new JSObject(); result.put("origin", origin); call.resolve(result);
63+
getActivity().runOnUiThread(() -> { Intent intent = getActivity().getIntent(); getActivity().finish(); getActivity().startActivity(intent); });
64+
}
65+
66+
@PluginMethod
67+
public void clearServer(PluginCall call) {
68+
getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().remove(SERVER).apply(); call.resolve();
69+
getActivity().runOnUiThread(() -> { Intent intent = getActivity().getIntent(); getActivity().finish(); getActivity().startActivity(intent); });
70+
}
71+
}
Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
package com.gitcommit90.onehelm.mobile;
22

3+
import android.content.Context;
4+
import android.os.Bundle;
35
import com.getcapacitor.BridgeActivity;
6+
import com.getcapacitor.CapConfig;
47

5-
public class MainActivity extends BridgeActivity {}
8+
public class MainActivity extends BridgeActivity {
9+
@Override
10+
protected void onCreate(Bundle savedInstanceState) {
11+
registerPlugin(InstanceGatewayPlugin.class);
12+
String selected = InstanceGatewayPlugin.validOrigin(getSharedPreferences(InstanceGatewayPlugin.PREFS, Context.MODE_PRIVATE).getString(InstanceGatewayPlugin.SERVER, null));
13+
if (selected != null) {
14+
config = new CapConfig.Builder(this)
15+
.setServerUrl(selected)
16+
.setErrorPath("error.html")
17+
.setAndroidScheme("https")
18+
.setAppendedUserAgentString("1HelmMobile")
19+
.setBackgroundColor("#111318")
20+
.setAllowMixedContent(false)
21+
.setWebContentsDebuggingEnabled(false)
22+
.setUseLegacyBridge(false)
23+
.setZoomableWebView(false)
24+
.create();
25+
}
26+
super.onCreate(savedInstanceState);
27+
}
28+
}

capacitor.config.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"appId": "com.gitcommit90.onehelm.mobile",
33
"appName": "1Helm",
4-
"webDir": "public",
4+
"webDir": "mobile-gateway",
55
"loggingBehavior": "none",
66
"backgroundColor": "#111318",
77
"zoomEnabled": false,
@@ -10,7 +10,8 @@
1010
"hostname": "localhost",
1111
"iosScheme": "capacitor",
1212
"androidScheme": "https",
13-
"cleartext": false
13+
"cleartext": false,
14+
"errorPath": "error.html"
1415
},
1516
"ios": {
1617
"contentInset": "never",

ios/App/App.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; };
1212
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
1313
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
14+
A1E100022026072800000001 /* GatewayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E100022026072800000002 /* GatewayViewController.swift */; };
1415
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
1516
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
1617
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
@@ -23,6 +24,7 @@
2324
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
2425
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
2526
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
27+
A1E100022026072800000002 /* GatewayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GatewayViewController.swift; sourceTree = "<group>"; };
2628
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
2729
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2830
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -66,6 +68,7 @@
6668
children = (
6769
50379B222058CBB4000EE86E /* capacitor.config.json */,
6870
504EC3071FED79650016851F /* AppDelegate.swift */,
71+
A1E100022026072800000002 /* GatewayViewController.swift */,
6972
504EC30B1FED79650016851F /* Main.storyboard */,
7073
504EC30E1FED79650016851F /* Assets.xcassets */,
7174
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
@@ -160,6 +163,7 @@
160163
buildActionMask = 2147483647;
161164
files = (
162165
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
166+
A1E100022026072800000001 /* GatewayViewController.swift in Sources */,
163167
);
164168
runOnlyForDeploymentPostprocessing = 0;
165169
};
@@ -302,14 +306,14 @@
302306
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
303307
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
304308
CODE_SIGN_STYLE = Automatic;
305-
CURRENT_PROJECT_VERSION = 19;
309+
CURRENT_PROJECT_VERSION = 22;
306310
INFOPLIST_FILE = App/Info.plist;
307311
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
308312
LD_RUNPATH_SEARCH_PATHS = (
309313
"$(inherited)",
310314
"@executable_path/Frameworks",
311315
);
312-
MARKETING_VERSION = 0.0.19;
316+
MARKETING_VERSION = 0.0.22;
313317
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
314318
PRODUCT_BUNDLE_IDENTIFIER = com.gitcommit90.onehelm.mobile;
315319
PRODUCT_NAME = 1Helm;
@@ -326,14 +330,14 @@
326330
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
327331
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
328332
CODE_SIGN_STYLE = Automatic;
329-
CURRENT_PROJECT_VERSION = 19;
333+
CURRENT_PROJECT_VERSION = 22;
330334
INFOPLIST_FILE = App/Info.plist;
331335
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
332336
LD_RUNPATH_SEARCH_PATHS = (
333337
"$(inherited)",
334338
"@executable_path/Frameworks",
335339
);
336-
MARKETING_VERSION = 0.0.19;
340+
MARKETING_VERSION = 0.0.22;
337341
PRODUCT_BUNDLE_IDENTIFIER = com.gitcommit90.onehelm.mobile;
338342
PRODUCT_NAME = 1Helm;
339343
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

ios/App/App/Base.lproj/Main.storyboard

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<!--Bridge View Controller-->
1212
<scene sceneID="tne-QT-ifu">
1313
<objects>
14-
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
14+
<viewController id="BYZ-38-t0r" customClass="GatewayViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
1515
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
1616
</objects>
1717
</scene>

0 commit comments

Comments
 (0)