Skip to content
Closed
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
4 changes: 4 additions & 0 deletions crates/ironclaw_gateway/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub const APP_JS: &str = concat!(
"\n",
include_str!("../static/js/surfaces/settings.js"),
"\n",
include_str!("../static/js/surfaces/install.js"),
"\n",
include_str!("../static/js/core/ui-helpers.js"),
"\n",
include_str!("../static/js/surfaces/config.js"),
Expand Down Expand Up @@ -121,6 +123,8 @@ pub const STYLE_CSS: &str = concat!(
include_str!("../static/styles/surfaces/tool-permissions.css"),
"\n",
include_str!("../static/styles/surfaces/projects.css"),
"\n",
include_str!("../static/styles/surfaces/install.css"),
);

/// Theme initialization script (runs synchronously in `<head>` to prevent FOUC).
Expand Down
37 changes: 37 additions & 0 deletions crates/ironclaw_gateway/static/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,43 @@ I18n.register('en', {
'thread.heartbeatAlerts': 'Heartbeat Alerts',
'thread.routine': 'Routine',

// IronHub deep-link install
'ironhub.install.verifyingTitle': 'Verifying install request',
'ironhub.install.verifying': 'Checking the signature on this install link...',
'ironhub.install.confirmTitle': 'Install from IronHub?',
'ironhub.install.fromHub': 'This install was requested from IronHub. Review it before installing.',
'ironhub.install.name': 'Name',
'ironhub.install.kind': 'Type',
'ironhub.install.version': 'Version',
'ironhub.install.release': 'Release',
'ironhub.install.confirm': 'Install',
'ironhub.install.cancel': 'Cancel',
'ironhub.install.close': 'Close',
'ironhub.install.installing': 'Installing...',
'ironhub.install.doneTitle': 'Installed',
'ironhub.install.success': 'Installed {name}',
'ironhub.install.failed': 'Install failed: {message}',
'ironhub.install.unverifiedTitle': 'Install link could not be verified',
'ironhub.install.unverified': 'This install link could not be verified. It may be expired, tampered with, or sent from a source your agent does not trust.',
'ironhub.install.missingParams': 'This install link is missing required parameters.',
'ironhub.install.trustLabel': 'Trust',
'ironhub.install.communityWarning': 'This is community-submitted content that has not been verified by the NEAR team. Review it carefully before installing.',
'ironhub.install.ackUnverified': 'I understand this is unverified community content and want to install anyway.',
'ironhub.signingKey.title': 'IronHub Integration',
'ironhub.signingKey.label': 'Shared key',
'ironhub.signingKey.description': 'Generate a key in your IronHub profile, then paste it here so install requests from IronHub can be verified by this agent.',
'ironhub.signingKey.placeholder': 'ihub_sk_...',
'ironhub.signingKey.save': 'Save Key',
'ironhub.signingKey.replace': 'Replace Key',
'ironhub.signingKey.revoke': 'Revoke Key',
'ironhub.signingKey.none': 'No signing key configured.',
'ironhub.signingKey.active': 'Active key fingerprint: {fingerprint} (created {created})',
'ironhub.signingKey.saved': 'Signing key saved.',
'ironhub.signingKey.revoked': 'Signing key revoked.',
'ironhub.signingKey.invalidPrefix': 'Key must start with ihub_sk_',
'ironhub.signingKey.invalidLength': 'Key must be at least 32 characters',
'ironhub.signingKey.error': 'Signing key operation failed: {message}',

// Extensions (dynamic)
'extensions.openingAuth': 'Opening authentication for {name}',
'extensions.installFailed': 'Install failed: {message}',
Expand Down
4 changes: 4 additions & 0 deletions crates/ironclaw_gateway/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ <h3 data-i18n="config.modelProviders">Model Providers</h3>
<div class="extensions-container" id="settings-agent-content">
<div class="empty-state" data-i18n="common.loading">Loading settings...</div>
</div>
<div class="extensions-container" id="settings-ironhub-card"></div>
</div>
<div class="settings-subpanel" id="settings-channels">
<div class="extensions-container" id="settings-channels-content">
Expand Down Expand Up @@ -570,6 +571,9 @@ <h3 data-i18n="settings.tools">Tool Permissions</h3>
</div>
</div>
</div>

<!-- IronHub deep-link install (no tab-bar button; reached via #/install/<tool>) -->
<div class="tab-panel" id="tab-install"></div>
</div>

<!-- Confirmation Modal -->
Expand Down
18 changes: 18 additions & 0 deletions crates/ironclaw_gateway/static/js/core/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ function restoreFromHash() {
case 'settings':
switchSettingsSubtab(state.detail);
break;
case 'install': {
var raw = state.detail || '';
var qIdx = raw.indexOf('?');
var pathSlug = qIdx >= 0 ? raw.substring(0, qIdx) : raw;
var qs = qIdx >= 0 ? raw.substring(qIdx + 1) : '';
var params = new URLSearchParams(qs);
startIronhubInstall({
slug: params.get('slug') || decodeURIComponent(pathSlug),
version: params.get('version'),
uid: params.get('uid'),
aid: params.get('aid'),
ts: params.get('ts'),
nonce: params.get('nonce'),
sig: params.get('sig'),
artifact_digest: params.get('artifact_digest'),
});
break;
}
}
}

Expand Down
203 changes: 203 additions & 0 deletions crates/ironclaw_gateway/static/js/surfaces/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
function ironhubInstallPanel() {
return document.getElementById('tab-install');
}

function renderIronhubInstallState(html) {
var panel = ironhubInstallPanel();
if (!panel) return;
panel.innerHTML = '<div class="ext-card ironhub-install-card">' + html + '</div>';
wireInstallActions();
}

function wireInstallActions() {
document.querySelectorAll('.ironhub-install-cancel').forEach(function(btn) {
btn.addEventListener('click', ironhubInstallCancel);
});
var ackCb = document.getElementById('ironhub-install-ack-cb');
if (ackCb) ackCb.addEventListener('change', ironhubAckChanged);
}

function ironhubInstallCancel() {
switchTab('chat');
}

function renderIronhubInstallError(message) {
renderIronhubInstallState(
'<h2>' + escapeHtml(I18n.t('ironhub.install.unverifiedTitle')) + '</h2>' +
'<p class="ironhub-install-error">' + escapeHtml(message) + '</p>' +
'<button class="btn-ext ironhub-install-cancel">' +
escapeHtml(I18n.t('ironhub.install.close')) + '</button>'
);
}

function renderIronhubConfirm(signed, info) {
var tool = signed && signed.slug ? signed.slug : '';
var kind = info && info.kind ? info.kind : 'tool';
var version = info && info.version ? info.version : '';
var description = info && info.description ? info.description : '';
var release = info && info.release_tag ? info.release_tag : '';
var provenance = info && info.provenance ? info.provenance : '';
var trustLabel = info && info.trust_label ? info.trust_label : '';
var isCommunityUnverified = provenance === 'new';
var rows = '';
rows += '<div class="ironhub-install-row"><span>' +
escapeHtml(I18n.t('ironhub.install.name')) + '</span><strong>' +
escapeHtml(tool) + '</strong></div>';
rows += '<div class="ironhub-install-row"><span>' +
escapeHtml(I18n.t('ironhub.install.kind')) + '</span><span>' +
escapeHtml(kind) + '</span></div>';
if (version) {
rows += '<div class="ironhub-install-row"><span>' +
escapeHtml(I18n.t('ironhub.install.version')) + '</span><span>' +
escapeHtml(version) + '</span></div>';
}
if (release) {
rows += '<div class="ironhub-install-row"><span>' +
escapeHtml(I18n.t('ironhub.install.release')) + '</span><span>' +
escapeHtml(release) + '</span></div>';
}
if (trustLabel) {
rows += '<div class="ironhub-install-row"><span>' +
escapeHtml(I18n.t('ironhub.install.trustLabel')) + '</span><span>' +
escapeHtml(trustLabel) + '</span></div>';
}
if (description) {
rows += '<p class="ironhub-install-desc">' + escapeHtml(description) + '</p>';
}

var warning = '';
var ackCheckbox = '';
var confirmDisabled = '';
if (isCommunityUnverified) {
warning = '<p class="ironhub-install-error">' +
escapeHtml(I18n.t('ironhub.install.communityWarning')) + '</p>';
ackCheckbox =
'<label class="ironhub-install-row"><input type="checkbox" ' +
'id="ironhub-install-ack-cb" /> ' +
escapeHtml(I18n.t('ironhub.install.ackUnverified')) + '</label>';
confirmDisabled = ' disabled';
}

renderIronhubInstallState(
'<h2>' + escapeHtml(I18n.t('ironhub.install.confirmTitle')) + '</h2>' +
'<p class="ironhub-install-source">' +
escapeHtml(I18n.t('ironhub.install.fromHub')) + '</p>' +
rows +
warning +
ackCheckbox +
'<div class="ironhub-install-actions">' +
'<button class="btn-ext install" id="ironhub-install-confirm-btn"' + confirmDisabled + '>' +
escapeHtml(I18n.t('ironhub.install.confirm')) + '</button>' +
'<button class="btn-ext ironhub-install-cancel">' +
escapeHtml(I18n.t('ironhub.install.cancel')) + '</button>' +
'</div>'
);
var btn = document.getElementById('ironhub-install-confirm-btn');
if (btn) {
btn.addEventListener('click', function() {
ironhubInstallConfirm(signed, isCommunityUnverified);
});
}
}

function ironhubAckChanged() {
var cb = document.getElementById('ironhub-install-ack-cb');
var btn = document.getElementById('ironhub-install-confirm-btn');
if (cb && btn) {
btn.disabled = !cb.checked;
}
}

function ironhubInstallConfirm(signed, requireAck) {
var btn = document.getElementById('ironhub-install-confirm-btn');
if (btn) {
btn.disabled = true;
btn.textContent = I18n.t('ironhub.install.installing');
}
var body = {
slug: signed.slug,
version: signed.version,
uid: signed.uid,
aid: signed.aid,
ts: parseInt(signed.ts, 10),
nonce: signed.nonce,
sig: signed.sig,
artifact_digest: signed.artifact_digest,
};
if (requireAck) {
body.acknowledge_unverified = true;
}
apiFetch('/api/ironhub/install', {
method: 'POST',
body: body,
}).then(function(res) {
var name = res && res.name ? res.name : signed.slug;
showToast(I18n.t('ironhub.install.success', { name: name }), 'success');
renderIronhubInstallState(
'<h2>' + escapeHtml(I18n.t('ironhub.install.doneTitle')) + '</h2>' +
'<p>' + escapeHtml(I18n.t('ironhub.install.success', { name: name })) + '</p>' +
'<button class="btn-ext ironhub-install-cancel">' +
escapeHtml(I18n.t('ironhub.install.close')) + '</button>'
);
}).catch(function(err) {
var msg = err && err.message ? err.message : 'unknown error';
showToast(I18n.t('ironhub.install.failed', { message: msg }), 'error');
if (btn) {
btn.disabled = false;
btn.textContent = I18n.t('ironhub.install.confirm');
}
});
}

function startIronhubInstall(params) {
renderIronhubInstallState(
'<h2>' + escapeHtml(I18n.t('ironhub.install.verifyingTitle')) + '</h2>' +
'<p>' + escapeHtml(I18n.t('ironhub.install.verifying')) + '</p>'
);

var slug = params && params.slug;
var version = params && params.version;
var uid = params && params.uid;
var aid = params && params.aid;
var ts = params && params.ts;
var nonce = params && params.nonce;
var sig = params && params.sig;
var artifactDigest = params && params.artifact_digest;

if (!slug || !version || !uid || !aid || !ts || !nonce || !sig || !artifactDigest) {
renderIronhubInstallError(I18n.t('ironhub.install.missingParams'));
return;
}

var signed = {
slug: slug,
version: version,
uid: uid,
aid: aid,
ts: parseInt(ts, 10),
nonce: nonce,
sig: sig,
artifact_digest: artifactDigest,
};

apiFetch('/api/ironhub/verify-intent', {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new deep-link install route has no browser/e2e coverage for hash parsing, verify-intent, info fetch, and confirm POST behavior. Handler tests will not catch regressions in this user-visible flow. Add an e2e/browser test that drives #/install/<name>?ts=<ts>&sig=<sig>, including invalid links and successful confirm.

method: 'POST',
body: signed,
}).then(function(res) {
if (!res || !res.valid) {
var reason = res && res.reason ? res.reason : I18n.t('ironhub.install.unverified');
renderIronhubInstallError(reason);
return;
}
return apiFetch('/api/ironhub/info?name=' + encodeURIComponent(slug))
.then(function(info) {
renderIronhubConfirm(signed, info);
})
.catch(function() {
renderIronhubConfirm(signed, null);
});
}).catch(function(err) {
var msg = err && err.message ? err.message : I18n.t('ironhub.install.unverified');
renderIronhubInstallError(msg);
});
}
Loading
Loading