arc402-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -0
- package/dist/abis.d.ts +19 -0
- package/dist/abis.d.ts.map +1 -0
- package/dist/abis.js +177 -0
- package/dist/abis.js.map +1 -0
- package/dist/bundler.d.ts +65 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +181 -0
- package/dist/bundler.js.map +1 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +24 -0
- package/dist/client.js.map +1 -0
- package/dist/coinbase-smart-wallet.d.ts +28 -0
- package/dist/coinbase-smart-wallet.d.ts.map +1 -0
- package/dist/coinbase-smart-wallet.js +38 -0
- package/dist/coinbase-smart-wallet.js.map +1 -0
- package/dist/commands/accept.d.ts +3 -0
- package/dist/commands/accept.d.ts.map +1 -0
- package/dist/commands/accept.js +26 -0
- package/dist/commands/accept.js.map +1 -0
- package/dist/commands/agent-handshake.d.ts +3 -0
- package/dist/commands/agent-handshake.d.ts.map +1 -0
- package/dist/commands/agent-handshake.js +61 -0
- package/dist/commands/agent-handshake.js.map +1 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +417 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/agreements.d.ts +3 -0
- package/dist/commands/agreements.d.ts.map +1 -0
- package/dist/commands/agreements.js +344 -0
- package/dist/commands/agreements.js.map +1 -0
- package/dist/commands/arbitrator.d.ts +3 -0
- package/dist/commands/arbitrator.d.ts.map +1 -0
- package/dist/commands/arbitrator.js +157 -0
- package/dist/commands/arbitrator.js.map +1 -0
- package/dist/commands/arena-handshake.d.ts +3 -0
- package/dist/commands/arena-handshake.d.ts.map +1 -0
- package/dist/commands/arena-handshake.js +187 -0
- package/dist/commands/arena-handshake.js.map +1 -0
- package/dist/commands/cancel.d.ts +3 -0
- package/dist/commands/cancel.d.ts.map +1 -0
- package/dist/commands/cancel.js +30 -0
- package/dist/commands/cancel.js.map +1 -0
- package/dist/commands/channel.d.ts +3 -0
- package/dist/commands/channel.d.ts.map +1 -0
- package/dist/commands/channel.js +238 -0
- package/dist/commands/channel.js.map +1 -0
- package/dist/commands/coldstart.d.ts +3 -0
- package/dist/commands/coldstart.d.ts.map +1 -0
- package/dist/commands/coldstart.js +148 -0
- package/dist/commands/coldstart.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +40 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/contract-interaction.d.ts +3 -0
- package/dist/commands/contract-interaction.d.ts.map +1 -0
- package/dist/commands/contract-interaction.js +165 -0
- package/dist/commands/contract-interaction.js.map +1 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +891 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/deliver.d.ts +3 -0
- package/dist/commands/deliver.d.ts.map +1 -0
- package/dist/commands/deliver.js +156 -0
- package/dist/commands/deliver.js.map +1 -0
- package/dist/commands/discover.d.ts +3 -0
- package/dist/commands/discover.d.ts.map +1 -0
- package/dist/commands/discover.js +224 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/dispute.d.ts +3 -0
- package/dist/commands/dispute.d.ts.map +1 -0
- package/dist/commands/dispute.js +348 -0
- package/dist/commands/dispute.js.map +1 -0
- package/dist/commands/endpoint.d.ts +3 -0
- package/dist/commands/endpoint.d.ts.map +1 -0
- package/dist/commands/endpoint.js +604 -0
- package/dist/commands/endpoint.js.map +1 -0
- package/dist/commands/hire.d.ts +3 -0
- package/dist/commands/hire.d.ts.map +1 -0
- package/dist/commands/hire.js +189 -0
- package/dist/commands/hire.js.map +1 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +163 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/negotiate.d.ts +3 -0
- package/dist/commands/negotiate.d.ts.map +1 -0
- package/dist/commands/negotiate.js +247 -0
- package/dist/commands/negotiate.js.map +1 -0
- package/dist/commands/openshell.d.ts +3 -0
- package/dist/commands/openshell.d.ts.map +1 -0
- package/dist/commands/openshell.js +952 -0
- package/dist/commands/openshell.js.map +1 -0
- package/dist/commands/owner.d.ts +3 -0
- package/dist/commands/owner.d.ts.map +1 -0
- package/dist/commands/owner.js +32 -0
- package/dist/commands/owner.js.map +1 -0
- package/dist/commands/policy.d.ts +4 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +248 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/relay.d.ts +3 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +279 -0
- package/dist/commands/relay.js.map +1 -0
- package/dist/commands/remediate.d.ts +3 -0
- package/dist/commands/remediate.d.ts.map +1 -0
- package/dist/commands/remediate.js +42 -0
- package/dist/commands/remediate.js.map +1 -0
- package/dist/commands/reputation.d.ts +4 -0
- package/dist/commands/reputation.d.ts.map +1 -0
- package/dist/commands/reputation.js +72 -0
- package/dist/commands/reputation.js.map +1 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +332 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/trust.d.ts +3 -0
- package/dist/commands/trust.d.ts.map +1 -0
- package/dist/commands/trust.js +23 -0
- package/dist/commands/trust.js.map +1 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +88 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/wallet.d.ts +3 -0
- package/dist/commands/wallet.d.ts.map +1 -0
- package/dist/commands/wallet.js +2520 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/commands/watchtower.d.ts +3 -0
- package/dist/commands/watchtower.d.ts.map +1 -0
- package/dist/commands/watchtower.js +238 -0
- package/dist/commands/watchtower.js.map +1 -0
- package/dist/commands/workroom.d.ts +3 -0
- package/dist/commands/workroom.d.ts.map +1 -0
- package/dist/commands/workroom.js +855 -0
- package/dist/commands/workroom.js.map +1 -0
- package/dist/config.d.ts +62 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +141 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/config.d.ts +74 -0
- package/dist/daemon/config.d.ts.map +1 -0
- package/dist/daemon/config.js +271 -0
- package/dist/daemon/config.js.map +1 -0
- package/dist/daemon/hire-listener.d.ts +31 -0
- package/dist/daemon/hire-listener.d.ts.map +1 -0
- package/dist/daemon/hire-listener.js +207 -0
- package/dist/daemon/hire-listener.js.map +1 -0
- package/dist/daemon/index.d.ts +29 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +535 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/job-lifecycle.d.ts +62 -0
- package/dist/daemon/job-lifecycle.d.ts.map +1 -0
- package/dist/daemon/job-lifecycle.js +201 -0
- package/dist/daemon/job-lifecycle.js.map +1 -0
- package/dist/daemon/notify.d.ts +22 -0
- package/dist/daemon/notify.d.ts.map +1 -0
- package/dist/daemon/notify.js +148 -0
- package/dist/daemon/notify.js.map +1 -0
- package/dist/daemon/token-metering.d.ts +42 -0
- package/dist/daemon/token-metering.d.ts.map +1 -0
- package/dist/daemon/token-metering.js +178 -0
- package/dist/daemon/token-metering.js.map +1 -0
- package/dist/daemon/userops.d.ts +21 -0
- package/dist/daemon/userops.d.ts.map +1 -0
- package/dist/daemon/userops.js +88 -0
- package/dist/daemon/userops.js.map +1 -0
- package/dist/daemon/wallet-monitor.d.ts +16 -0
- package/dist/daemon/wallet-monitor.d.ts.map +1 -0
- package/dist/daemon/wallet-monitor.js +57 -0
- package/dist/daemon/wallet-monitor.js.map +1 -0
- package/dist/drain-v4.d.ts +2 -0
- package/dist/drain-v4.d.ts.map +1 -0
- package/dist/drain-v4.js +167 -0
- package/dist/drain-v4.js.map +1 -0
- package/dist/endpoint-config.d.ts +36 -0
- package/dist/endpoint-config.d.ts.map +1 -0
- package/dist/endpoint-config.js +96 -0
- package/dist/endpoint-config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/openshell-runtime.d.ts +55 -0
- package/dist/openshell-runtime.d.ts.map +1 -0
- package/dist/openshell-runtime.js +268 -0
- package/dist/openshell-runtime.js.map +1 -0
- package/dist/signing.d.ts +2 -0
- package/dist/signing.d.ts.map +1 -0
- package/dist/signing.js +23 -0
- package/dist/signing.js.map +1 -0
- package/dist/telegram-notify.d.ts +23 -0
- package/dist/telegram-notify.d.ts.map +1 -0
- package/dist/telegram-notify.js +106 -0
- package/dist/telegram-notify.js.map +1 -0
- package/dist/ui/banner.d.ts +7 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +37 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/colors.d.ts +14 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +29 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/format.d.ts +26 -0
- package/dist/ui/format.d.ts.map +1 -0
- package/dist/ui/format.js +77 -0
- package/dist/ui/format.js.map +1 -0
- package/dist/ui/spinner.d.ts +8 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +43 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/utils/format.d.ts +10 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +61 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/hash.d.ts +3 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +43 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +21 -0
- package/dist/utils/time.js.map +1 -0
- package/dist/wallet-router.d.ts +25 -0
- package/dist/wallet-router.d.ts.map +1 -0
- package/dist/wallet-router.js +153 -0
- package/dist/wallet-router.js.map +1 -0
- package/dist/walletconnect-session.d.ts +12 -0
- package/dist/walletconnect-session.d.ts.map +1 -0
- package/dist/walletconnect-session.js +26 -0
- package/dist/walletconnect-session.js.map +1 -0
- package/dist/walletconnect.d.ts +46 -0
- package/dist/walletconnect.d.ts.map +1 -0
- package/dist/walletconnect.js +267 -0
- package/dist/walletconnect.js.map +1 -0
- package/package.json +38 -0
- package/scripts/authorize-machine-key.ts +43 -0
- package/scripts/drain-wallet.ts +149 -0
- package/scripts/execute-spend-only.ts +81 -0
- package/scripts/register-agent-userop.ts +186 -0
- package/src/abis.ts +187 -0
- package/src/bundler.ts +235 -0
- package/src/client.ts +34 -0
- package/src/coinbase-smart-wallet.ts +51 -0
- package/src/commands/accept.ts +25 -0
- package/src/commands/agent-handshake.ts +67 -0
- package/src/commands/agent.ts +458 -0
- package/src/commands/agreements.ts +324 -0
- package/src/commands/arbitrator.ts +129 -0
- package/src/commands/arena-handshake.ts +217 -0
- package/src/commands/cancel.ts +26 -0
- package/src/commands/channel.ts +208 -0
- package/src/commands/coldstart.ts +156 -0
- package/src/commands/config.ts +35 -0
- package/src/commands/contract-interaction.ts +166 -0
- package/src/commands/daemon.ts +971 -0
- package/src/commands/deliver.ts +116 -0
- package/src/commands/discover.ts +295 -0
- package/src/commands/dispute.ts +373 -0
- package/src/commands/endpoint.ts +619 -0
- package/src/commands/hire.ts +200 -0
- package/src/commands/migrate.ts +175 -0
- package/src/commands/negotiate.ts +270 -0
- package/src/commands/openshell.ts +1053 -0
- package/src/commands/owner.ts +30 -0
- package/src/commands/policy.ts +252 -0
- package/src/commands/relay.ts +272 -0
- package/src/commands/remediate.ts +22 -0
- package/src/commands/reputation.ts +71 -0
- package/src/commands/setup.ts +343 -0
- package/src/commands/trust.ts +15 -0
- package/src/commands/verify.ts +88 -0
- package/src/commands/wallet.ts +2892 -0
- package/src/commands/watchtower.ts +232 -0
- package/src/commands/workroom.ts +889 -0
- package/src/config.ts +153 -0
- package/src/daemon/config.ts +308 -0
- package/src/daemon/hire-listener.ts +226 -0
- package/src/daemon/index.ts +609 -0
- package/src/daemon/job-lifecycle.ts +215 -0
- package/src/daemon/notify.ts +157 -0
- package/src/daemon/token-metering.ts +183 -0
- package/src/daemon/userops.ts +119 -0
- package/src/daemon/wallet-monitor.ts +90 -0
- package/src/drain-v4.ts +159 -0
- package/src/endpoint-config.ts +83 -0
- package/src/index.ts +75 -0
- package/src/openshell-runtime.ts +277 -0
- package/src/signing.ts +28 -0
- package/src/telegram-notify.ts +88 -0
- package/src/ui/banner.ts +41 -0
- package/src/ui/colors.ts +30 -0
- package/src/ui/format.ts +77 -0
- package/src/ui/spinner.ts +46 -0
- package/src/utils/format.ts +48 -0
- package/src/utils/hash.ts +5 -0
- package/src/utils/time.ts +15 -0
- package/src/wallet-router.ts +178 -0
- package/src/walletconnect-session.ts +27 -0
- package/src/walletconnect.ts +294 -0
- package/test/time.test.js +11 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet monitor — verifies wallet contract exists and is operational.
|
|
3
|
+
* Steps 4 and 5 of the daemon startup sequence (Spec 32 §3).
|
|
4
|
+
*/
|
|
5
|
+
import { ethers } from "ethers";
|
|
6
|
+
import type { DaemonConfig } from "./config";
|
|
7
|
+
import {
|
|
8
|
+
ARC402_WALLET_GUARDIAN_ABI,
|
|
9
|
+
ARC402_WALLET_MACHINE_KEY_ABI,
|
|
10
|
+
} from "../abis";
|
|
11
|
+
|
|
12
|
+
export interface WalletStatus {
|
|
13
|
+
contractAddress: string;
|
|
14
|
+
ownerAddress: string;
|
|
15
|
+
isFrozen: boolean;
|
|
16
|
+
machineKeyAuthorized: boolean;
|
|
17
|
+
ethBalance: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function verifyWallet(
|
|
21
|
+
config: DaemonConfig,
|
|
22
|
+
provider: ethers.Provider,
|
|
23
|
+
machineKeyAddress: string
|
|
24
|
+
): Promise<WalletStatus> {
|
|
25
|
+
const { contract_address, owner_address } = config.wallet;
|
|
26
|
+
|
|
27
|
+
// Step 4: Verify wallet contract exists
|
|
28
|
+
const code = await provider.getCode(contract_address);
|
|
29
|
+
if (code === "0x") {
|
|
30
|
+
throw new Error(`No contract at wallet address ${contract_address}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const guardianContract = new ethers.Contract(
|
|
34
|
+
contract_address,
|
|
35
|
+
ARC402_WALLET_GUARDIAN_ABI,
|
|
36
|
+
provider
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Verify owner matches config (if owner_address is set)
|
|
40
|
+
if (owner_address) {
|
|
41
|
+
const onChainOwner: string = await guardianContract.owner();
|
|
42
|
+
if (onChainOwner.toLowerCase() !== owner_address.toLowerCase()) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Wallet owner mismatch. Config: ${owner_address}, On-chain: ${onChainOwner}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onChainOwner: string = await guardianContract.owner();
|
|
50
|
+
|
|
51
|
+
// Step 5: Check wallet is not frozen
|
|
52
|
+
const frozen: boolean = await guardianContract.frozen();
|
|
53
|
+
if (frozen) {
|
|
54
|
+
throw new Error("Wallet is frozen. Daemon cannot operate.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check machine key authorization (best effort — v1 wallets may not have registry)
|
|
58
|
+
let machineKeyAuthorized = false;
|
|
59
|
+
try {
|
|
60
|
+
const machineKeyContract = new ethers.Contract(
|
|
61
|
+
contract_address,
|
|
62
|
+
ARC402_WALLET_MACHINE_KEY_ABI,
|
|
63
|
+
provider
|
|
64
|
+
);
|
|
65
|
+
machineKeyAuthorized = await machineKeyContract.authorizedMachineKeys(machineKeyAddress);
|
|
66
|
+
} catch {
|
|
67
|
+
// v1 wallet may not have machine key registry — policy-based auth, continue
|
|
68
|
+
machineKeyAuthorized = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get ETH balance
|
|
72
|
+
const balanceBig = await provider.getBalance(contract_address);
|
|
73
|
+
const ethBalance = ethers.formatEther(balanceBig);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
contractAddress: contract_address,
|
|
77
|
+
ownerAddress: onChainOwner,
|
|
78
|
+
isFrozen: false,
|
|
79
|
+
machineKeyAuthorized,
|
|
80
|
+
ethBalance,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getWalletBalance(
|
|
85
|
+
contractAddress: string,
|
|
86
|
+
provider: ethers.Provider
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const balanceBig = await provider.getBalance(contractAddress);
|
|
89
|
+
return ethers.formatEther(balanceBig);
|
|
90
|
+
}
|
package/src/drain-v4.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drain-v4.ts
|
|
3
|
+
* Drains ETH from v4 wallet (0xb4aF8760) to owner (0x7745772d) via WalletConnect.
|
|
4
|
+
* Steps: openContext → attest → executeSpend → closeContext
|
|
5
|
+
*/
|
|
6
|
+
import { ethers } from "ethers";
|
|
7
|
+
import { SignClient } from "@walletconnect/sign-client";
|
|
8
|
+
import { KeyValueStorage } from "@walletconnect/keyvaluestorage";
|
|
9
|
+
import qrcode from "qrcode-terminal";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
|
|
14
|
+
const V4_WALLET = "0xb4aF8760d349a6A4C8495Ae4da9089bC84994eE6";
|
|
15
|
+
const OWNER = "0x7745772d67Cd52c1F38706bF5550AdcD925c7c00";
|
|
16
|
+
const INTENT_ATTEST = "0x7ad8db6C5f394542E8e9658F86C85cC99Cf6D460";
|
|
17
|
+
const CHAIN_ID = 8453;
|
|
18
|
+
const RPC = "https://base-mainnet.g.alchemy.com/v2/YIA2uRCsFI-j5pqH-aRzflrACSlV1Qrs";
|
|
19
|
+
|
|
20
|
+
// Leave 0.00005 ETH for gas
|
|
21
|
+
const DRAIN_AMOUNT = ethers.parseEther("0.00045");
|
|
22
|
+
|
|
23
|
+
const WALLET_ABI = [
|
|
24
|
+
"function openContext(bytes32 contextId, string calldata taskType) external",
|
|
25
|
+
"function attest(bytes32 attestationId, string calldata action, string calldata reason, address recipient, uint256 amount, address token, uint256 expiresAt) external",
|
|
26
|
+
"function executeSpend(address payable recipient, uint256 amount, string calldata category, bytes32 attestationId) external",
|
|
27
|
+
"function closeContext() external",
|
|
28
|
+
"function contextOpen() external view returns (bool)",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const ATTEST_ABI = [
|
|
32
|
+
"function attest(bytes32 attestationId, string calldata action, string calldata reason, address recipient, uint256 amount, address token, uint256 expiresAt) external",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
async function main() {
|
|
36
|
+
const provider = new ethers.JsonRpcProvider(RPC);
|
|
37
|
+
const balance = await provider.getBalance(V4_WALLET);
|
|
38
|
+
console.log(`v4 balance: ${ethers.formatEther(balance)} ETH`);
|
|
39
|
+
|
|
40
|
+
if (balance < DRAIN_AMOUNT) {
|
|
41
|
+
console.error("Insufficient balance");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load config for machine key
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".arc402/config.json"), "utf8"));
|
|
47
|
+
const machineKey = new ethers.Wallet(config.privateKey, provider);
|
|
48
|
+
console.log(`Machine key: ${machineKey.address}`);
|
|
49
|
+
|
|
50
|
+
// WalletConnect setup
|
|
51
|
+
const projectId = config.walletConnectProjectId;
|
|
52
|
+
const storagePath = path.join(os.homedir(), ".arc402/wc-storage.json");
|
|
53
|
+
const client = await SignClient.init({
|
|
54
|
+
projectId,
|
|
55
|
+
metadata: { name: "ARC-402 Drain v4", description: "Drain v4 wallet ETH to owner", url: "https://app.arc402.xyz", icons: [] },
|
|
56
|
+
storage: new KeyValueStorage({ database: storagePath }),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { uri, approval } = await client.connect({
|
|
60
|
+
requiredNamespaces: {
|
|
61
|
+
eip155: {
|
|
62
|
+
methods: ["eth_sendTransaction"],
|
|
63
|
+
chains: [`eip155:${CHAIN_ID}`],
|
|
64
|
+
events: ["chainChanged", "accountsChanged"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log("\nScan this QR in MetaMask (make sure you're on Base):\n");
|
|
70
|
+
qrcode.generate(uri!, { small: true });
|
|
71
|
+
console.log(`\nMetaMask deep link:\nmetamask://wc?uri=${encodeURIComponent(uri!)}\n`);
|
|
72
|
+
console.log("Waiting for MetaMask approval...");
|
|
73
|
+
|
|
74
|
+
const session = await approval();
|
|
75
|
+
console.log("✓ MetaMask connected");
|
|
76
|
+
|
|
77
|
+
// Build transactions
|
|
78
|
+
const walletIface = new ethers.Interface(WALLET_ABI);
|
|
79
|
+
const attestIface = new ethers.Interface(ATTEST_ABI);
|
|
80
|
+
|
|
81
|
+
const contextId = ethers.keccak256(ethers.toUtf8Bytes(`drain-${Date.now()}`));
|
|
82
|
+
const attestationId = ethers.keccak256(ethers.toUtf8Bytes(`attest-${Date.now()}`));
|
|
83
|
+
const expiry = Math.floor(Date.now() / 1000) + 600; // 10 min
|
|
84
|
+
|
|
85
|
+
const account = session.namespaces.eip155.accounts[0].split(":")[2];
|
|
86
|
+
console.log(`Owner address: ${account}`);
|
|
87
|
+
|
|
88
|
+
async function sendTx(to: string, data: string, description: string) {
|
|
89
|
+
console.log(`\nSending tx: ${description}`);
|
|
90
|
+
const result = await client.request({
|
|
91
|
+
topic: session.topic,
|
|
92
|
+
chainId: `eip155:${CHAIN_ID}`,
|
|
93
|
+
request: {
|
|
94
|
+
method: "eth_sendTransaction",
|
|
95
|
+
params: [{ from: account, to, data, gas: "0x30D40" }],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
console.log(`✓ ${description}: ${result}`);
|
|
99
|
+
// Wait for confirmation
|
|
100
|
+
const receipt = await provider.waitForTransaction(result as string, 1, 60000);
|
|
101
|
+
console.log(` Confirmed in block ${receipt?.blockNumber}`);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 1: close stale context via machine key if needed
|
|
106
|
+
console.log("\nStep 1: Checking context state...");
|
|
107
|
+
const walletContract = new ethers.Contract(V4_WALLET, WALLET_ABI, machineKey);
|
|
108
|
+
const isOpen = await walletContract.contextOpen();
|
|
109
|
+
if (isOpen) {
|
|
110
|
+
console.log(" Stale context found — closing it...");
|
|
111
|
+
const closeTx = await walletContract.closeContext();
|
|
112
|
+
await closeTx.wait(2);
|
|
113
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
114
|
+
console.log(` ✓ Closed: ${closeTx.hash}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Step 2: owner opens context (MetaMask signs — so simulation sees it)
|
|
118
|
+
console.log("\nStep 2: Owner opens context via MetaMask...");
|
|
119
|
+
await sendTx(
|
|
120
|
+
V4_WALLET,
|
|
121
|
+
new ethers.Interface(WALLET_ABI).encodeFunctionData("openContext", [contextId, "drain"]),
|
|
122
|
+
"Open context"
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Step 3: call attest() directly on wallet (onlyOwnerOrMachineKey — no executeContractCall needed)
|
|
126
|
+
const walletIface2 = new ethers.Interface(WALLET_ABI);
|
|
127
|
+
await sendTx(
|
|
128
|
+
V4_WALLET,
|
|
129
|
+
walletIface2.encodeFunctionData("attest", [
|
|
130
|
+
attestationId,
|
|
131
|
+
"spend",
|
|
132
|
+
"drain v4 to owner",
|
|
133
|
+
OWNER,
|
|
134
|
+
DRAIN_AMOUNT,
|
|
135
|
+
ethers.ZeroAddress,
|
|
136
|
+
expiry,
|
|
137
|
+
]),
|
|
138
|
+
"Create spend attestation"
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Step 4: machine key executes spend
|
|
142
|
+
console.log("\nStep 4: Executing spend (machine key)...");
|
|
143
|
+
const spendTx = await walletContract.executeSpend(OWNER, DRAIN_AMOUNT, "general", attestationId);
|
|
144
|
+
await spendTx.wait();
|
|
145
|
+
console.log(`✓ ETH sent to owner: ${spendTx.hash}`);
|
|
146
|
+
|
|
147
|
+
// Step 4: close context
|
|
148
|
+
const closeTx = await walletContract.closeContext();
|
|
149
|
+
await closeTx.wait();
|
|
150
|
+
console.log(`✓ Context closed: ${closeTx.hash}`);
|
|
151
|
+
|
|
152
|
+
const newBalance = await provider.getBalance(V4_WALLET);
|
|
153
|
+
console.log(`\nv4 remaining balance: ${ethers.formatEther(newBalance)} ETH`);
|
|
154
|
+
console.log(`Done. ${ethers.formatEther(DRAIN_AMOUNT)} ETH sent to ${OWNER}`);
|
|
155
|
+
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
export const ARC402_DIR = path.join(os.homedir(), ".arc402");
|
|
6
|
+
export const ENDPOINT_CONFIG_PATH = path.join(ARC402_DIR, "endpoint.json");
|
|
7
|
+
export const DEFAULT_LOCAL_INGRESS_TARGET = "http://127.0.0.1:4402";
|
|
8
|
+
export const DEFAULT_TUNNEL_MODE = "host-cloudflared" as const;
|
|
9
|
+
|
|
10
|
+
export interface EndpointConfig {
|
|
11
|
+
version: 1;
|
|
12
|
+
agentName: string;
|
|
13
|
+
hostname: string;
|
|
14
|
+
publicUrl: string;
|
|
15
|
+
localIngressTarget: string;
|
|
16
|
+
tunnelMode: "host-cloudflared";
|
|
17
|
+
tunnelTarget?: string;
|
|
18
|
+
walletAddress?: string;
|
|
19
|
+
subdomainApi?: string;
|
|
20
|
+
notes?: string;
|
|
21
|
+
claimedAt?: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function endpointConfigExists(): boolean {
|
|
26
|
+
return fs.existsSync(ENDPOINT_CONFIG_PATH);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadEndpointConfig(): EndpointConfig | null {
|
|
30
|
+
if (!endpointConfigExists()) return null;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(ENDPOINT_CONFIG_PATH, "utf-8")) as EndpointConfig;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function saveEndpointConfig(config: EndpointConfig): void {
|
|
39
|
+
fs.mkdirSync(ARC402_DIR, { recursive: true, mode: 0o700 });
|
|
40
|
+
fs.writeFileSync(ENDPOINT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeAgentName(value: string): string {
|
|
44
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildHostname(agentName: string): string {
|
|
48
|
+
return `${normalizeAgentName(agentName)}.arc402.xyz`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildPublicUrl(hostname: string): string {
|
|
52
|
+
return `https://${hostname}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildEndpointConfig(input: {
|
|
56
|
+
agentName: string;
|
|
57
|
+
localIngressTarget?: string;
|
|
58
|
+
tunnelMode?: "host-cloudflared";
|
|
59
|
+
tunnelTarget?: string;
|
|
60
|
+
walletAddress?: string;
|
|
61
|
+
subdomainApi?: string;
|
|
62
|
+
notes?: string;
|
|
63
|
+
claimedAt?: string;
|
|
64
|
+
existing?: EndpointConfig | null;
|
|
65
|
+
}): EndpointConfig {
|
|
66
|
+
const normalizedName = normalizeAgentName(input.agentName);
|
|
67
|
+
const hostname = buildHostname(normalizedName);
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
return {
|
|
70
|
+
version: 1,
|
|
71
|
+
agentName: normalizedName,
|
|
72
|
+
hostname,
|
|
73
|
+
publicUrl: buildPublicUrl(hostname),
|
|
74
|
+
localIngressTarget: input.localIngressTarget ?? input.existing?.localIngressTarget ?? DEFAULT_LOCAL_INGRESS_TARGET,
|
|
75
|
+
tunnelMode: input.tunnelMode ?? input.existing?.tunnelMode ?? DEFAULT_TUNNEL_MODE,
|
|
76
|
+
tunnelTarget: input.tunnelTarget ?? input.existing?.tunnelTarget,
|
|
77
|
+
walletAddress: input.walletAddress ?? input.existing?.walletAddress,
|
|
78
|
+
subdomainApi: input.subdomainApi ?? input.existing?.subdomainApi,
|
|
79
|
+
notes: input.notes ?? input.existing?.notes,
|
|
80
|
+
claimedAt: input.claimedAt ?? input.existing?.claimedAt,
|
|
81
|
+
updatedAt: now,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerAcceptCommand } from "./commands/accept";
|
|
4
|
+
import { registerAgentCommands } from "./commands/agent";
|
|
5
|
+
import { registerAgreementsCommands } from "./commands/agreements";
|
|
6
|
+
import { registerArbitratorCommand } from "./commands/arbitrator";
|
|
7
|
+
import { registerCancelCommand } from "./commands/cancel";
|
|
8
|
+
import { registerChannelCommands } from "./commands/channel";
|
|
9
|
+
import { registerConfigCommands } from "./commands/config";
|
|
10
|
+
import { registerDeliverCommand } from "./commands/deliver";
|
|
11
|
+
import { registerDiscoverCommand } from "./commands/discover";
|
|
12
|
+
import { registerEndpointCommands } from "./commands/endpoint";
|
|
13
|
+
import { registerDisputeCommand } from "./commands/dispute";
|
|
14
|
+
import { registerHireCommand } from "./commands/hire";
|
|
15
|
+
import { registerHandshakeCommand } from "./commands/agent-handshake";
|
|
16
|
+
import { registerNegotiateCommands } from "./commands/negotiate";
|
|
17
|
+
import { registerRelayCommands } from "./commands/relay";
|
|
18
|
+
import { registerRemediateCommands } from "./commands/remediate";
|
|
19
|
+
import { registerDaemonCommands } from "./commands/daemon";
|
|
20
|
+
import { registerOpenShellCommands } from "./commands/openshell";
|
|
21
|
+
import { registerWorkroomCommands } from "./commands/workroom";
|
|
22
|
+
import { registerArenaHandshakeCommands } from "./commands/arena-handshake";
|
|
23
|
+
import { registerTrustCommand } from "./commands/trust";
|
|
24
|
+
import { registerWalletCommands } from "./commands/wallet";
|
|
25
|
+
import { renderBanner } from "./ui/banner";
|
|
26
|
+
import { registerOwnerCommands } from "./commands/owner";
|
|
27
|
+
import { registerSetupCommands } from "./commands/setup";
|
|
28
|
+
import { registerVerifyCommand } from "./commands/verify";
|
|
29
|
+
import { registerContractInteractionCommands } from "./commands/contract-interaction";
|
|
30
|
+
import { registerWatchtowerCommands } from "./commands/watchtower";
|
|
31
|
+
import { registerColdStartCommands } from "./commands/coldstart";
|
|
32
|
+
import { registerMigrateCommands } from "./commands/migrate";
|
|
33
|
+
import reputation from "./commands/reputation.js";
|
|
34
|
+
import policy from "./commands/policy.js";
|
|
35
|
+
|
|
36
|
+
// Show banner when invoked with no arguments
|
|
37
|
+
if (process.argv.length <= 2) {
|
|
38
|
+
renderBanner();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const program = new Command();
|
|
43
|
+
program.name("arc402").description("ARC-402 CLI aligned to canonical-capability discovery → negotiate → hire → remediate → dispute workflow").version("0.2.0");
|
|
44
|
+
registerConfigCommands(program);
|
|
45
|
+
registerHandshakeCommand(program);
|
|
46
|
+
registerAgentCommands(program);
|
|
47
|
+
registerDiscoverCommand(program);
|
|
48
|
+
registerEndpointCommands(program);
|
|
49
|
+
registerNegotiateCommands(program);
|
|
50
|
+
registerHireCommand(program);
|
|
51
|
+
registerAgreementsCommands(program);
|
|
52
|
+
registerAcceptCommand(program);
|
|
53
|
+
registerDeliverCommand(program);
|
|
54
|
+
registerRemediateCommands(program);
|
|
55
|
+
registerDisputeCommand(program);
|
|
56
|
+
registerArbitratorCommand(program);
|
|
57
|
+
registerCancelCommand(program);
|
|
58
|
+
registerChannelCommands(program);
|
|
59
|
+
registerRelayCommands(program);
|
|
60
|
+
registerDaemonCommands(program);
|
|
61
|
+
registerOpenShellCommands(program);
|
|
62
|
+
registerWorkroomCommands(program);
|
|
63
|
+
registerArenaHandshakeCommands(program);
|
|
64
|
+
registerTrustCommand(program);
|
|
65
|
+
registerWalletCommands(program);
|
|
66
|
+
registerOwnerCommands(program);
|
|
67
|
+
registerSetupCommands(program);
|
|
68
|
+
registerVerifyCommand(program);
|
|
69
|
+
registerContractInteractionCommands(program);
|
|
70
|
+
registerWatchtowerCommands(program);
|
|
71
|
+
registerColdStartCommands(program);
|
|
72
|
+
registerMigrateCommands(program);
|
|
73
|
+
program.addCommand(reputation);
|
|
74
|
+
program.addCommand(policy);
|
|
75
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { parse as parseToml } from "smol-toml";
|
|
6
|
+
import { loadConfig } from "./config";
|
|
7
|
+
|
|
8
|
+
export const ARC402_DIR = path.join(os.homedir(), ".arc402");
|
|
9
|
+
export const OPENSHELL_TOML = path.join(ARC402_DIR, "openshell.toml");
|
|
10
|
+
export const OPENSHELL_RUNTIME_DIR = path.join(ARC402_DIR, "openshell-runtime");
|
|
11
|
+
export const OPENSHELL_RUNTIME_TARBALL = path.join(OPENSHELL_RUNTIME_DIR, "arc402-cli-runtime.tgz");
|
|
12
|
+
export const DEFAULT_RUNTIME_REMOTE_ROOT = "/sandbox/.arc402/runtime/arc402-cli";
|
|
13
|
+
|
|
14
|
+
export interface OpenShellConfig {
|
|
15
|
+
sandbox: { name: string; policy?: string; providers?: string[] };
|
|
16
|
+
runtime?: {
|
|
17
|
+
local_tarball?: string;
|
|
18
|
+
remote_root?: string;
|
|
19
|
+
synced_at?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runCmd(
|
|
24
|
+
cmd: string,
|
|
25
|
+
args: string[],
|
|
26
|
+
opts: { env?: NodeJS.ProcessEnv; input?: string; timeout?: number } = {}
|
|
27
|
+
): { stdout: string; stderr: string; ok: boolean; status: number | null } {
|
|
28
|
+
const result = spawnSync(cmd, args, {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
env: { ...process.env, ...(opts.env ?? {}) },
|
|
31
|
+
input: opts.input,
|
|
32
|
+
timeout: opts.timeout ?? 60000,
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
stdout: (result.stdout ?? "").trim(),
|
|
36
|
+
stderr: (result.stderr ?? "").trim(),
|
|
37
|
+
ok: result.status === 0 && !result.error,
|
|
38
|
+
status: result.status,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DockerAccessStatus {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
detail: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function detectDockerAccess(): DockerAccessStatus {
|
|
48
|
+
const result = runCmd("docker", ["info", "--format", "{{.ServerVersion}}"], { timeout: 20000 });
|
|
49
|
+
if (result.ok) {
|
|
50
|
+
return { ok: true, detail: `running (${result.stdout || "version unknown"})` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const detail = result.stderr || result.stdout || "Docker is unavailable";
|
|
54
|
+
if (/permission denied/i.test(detail)) {
|
|
55
|
+
return { ok: false, detail: "installed but this shell cannot access the Docker daemon" };
|
|
56
|
+
}
|
|
57
|
+
if (/Cannot connect to the Docker daemon/i.test(detail) || /Is the docker daemon running/i.test(detail)) {
|
|
58
|
+
return { ok: false, detail: "installed but the Docker daemon is not running" };
|
|
59
|
+
}
|
|
60
|
+
if (/command not found/i.test(detail) || result.status === 127) {
|
|
61
|
+
return { ok: false, detail: "not installed" };
|
|
62
|
+
}
|
|
63
|
+
return { ok: false, detail };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveOpenShellSecrets(): {
|
|
67
|
+
machineKey?: string;
|
|
68
|
+
telegramBotToken?: string;
|
|
69
|
+
telegramChatId?: string;
|
|
70
|
+
} {
|
|
71
|
+
let machineKey = process.env["ARC402_MACHINE_KEY"];
|
|
72
|
+
let telegramBotToken = process.env["TELEGRAM_BOT_TOKEN"];
|
|
73
|
+
let telegramChatId = process.env["TELEGRAM_CHAT_ID"];
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const config = loadConfig();
|
|
77
|
+
if (!machineKey && config.privateKey) machineKey = config.privateKey;
|
|
78
|
+
if (!telegramBotToken && config.telegramBotToken) telegramBotToken = config.telegramBotToken;
|
|
79
|
+
if (!telegramChatId && config.telegramChatId) telegramChatId = config.telegramChatId;
|
|
80
|
+
} catch {
|
|
81
|
+
// CLI config is optional here; env vars still win when present.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { machineKey, telegramBotToken, telegramChatId };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildOpenShellSecretExports(requireMachineKey = false): string {
|
|
88
|
+
const secrets = resolveOpenShellSecrets();
|
|
89
|
+
const exports: string[] = [];
|
|
90
|
+
|
|
91
|
+
if (secrets.machineKey) {
|
|
92
|
+
exports.push(`export ARC402_MACHINE_KEY=${shellEscape(secrets.machineKey)}`);
|
|
93
|
+
} else if (requireMachineKey) {
|
|
94
|
+
throw new Error("ARC402 machine key not found in env or arc402 config");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (secrets.telegramBotToken) {
|
|
98
|
+
exports.push(`export TELEGRAM_BOT_TOKEN=${shellEscape(secrets.telegramBotToken)}`);
|
|
99
|
+
}
|
|
100
|
+
if (secrets.telegramChatId) {
|
|
101
|
+
exports.push(`export TELEGRAM_CHAT_ID=${shellEscape(secrets.telegramChatId)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return exports.join(" && ");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function readOpenShellConfig(): OpenShellConfig | null {
|
|
108
|
+
if (!fs.existsSync(OPENSHELL_TOML)) return null;
|
|
109
|
+
try {
|
|
110
|
+
const raw = fs.readFileSync(OPENSHELL_TOML, "utf-8");
|
|
111
|
+
const parsed = parseToml(raw) as Record<string, unknown>;
|
|
112
|
+
const sb = parsed.sandbox as Record<string, unknown> | undefined;
|
|
113
|
+
if (!sb || typeof sb.name !== "string") return null;
|
|
114
|
+
const runtime = parsed.runtime as Record<string, unknown> | undefined;
|
|
115
|
+
return {
|
|
116
|
+
sandbox: {
|
|
117
|
+
name: sb.name,
|
|
118
|
+
policy: typeof sb.policy === "string" ? sb.policy : undefined,
|
|
119
|
+
providers: Array.isArray(sb.providers) ? (sb.providers as string[]) : undefined,
|
|
120
|
+
},
|
|
121
|
+
runtime: runtime ? {
|
|
122
|
+
local_tarball: typeof runtime.local_tarball === "string" ? runtime.local_tarball : undefined,
|
|
123
|
+
remote_root: typeof runtime.remote_root === "string" ? runtime.remote_root : undefined,
|
|
124
|
+
synced_at: typeof runtime.synced_at === "string" ? runtime.synced_at : undefined,
|
|
125
|
+
} : undefined,
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function writeOpenShellConfig(config: OpenShellConfig): void {
|
|
133
|
+
fs.mkdirSync(ARC402_DIR, { recursive: true, mode: 0o700 });
|
|
134
|
+
const providers = (config.sandbox.providers ?? []).map((p) => `"${p}"`).join(", ");
|
|
135
|
+
const lines = [
|
|
136
|
+
"# ARC-402 OpenShell configuration",
|
|
137
|
+
"# Written by: arc402 openshell init / sync-runtime",
|
|
138
|
+
"",
|
|
139
|
+
"[sandbox]",
|
|
140
|
+
`name = "${config.sandbox.name}"`,
|
|
141
|
+
config.sandbox.policy ? `policy = "${config.sandbox.policy}"` : "",
|
|
142
|
+
`providers = [${providers}]`,
|
|
143
|
+
"",
|
|
144
|
+
"[runtime]",
|
|
145
|
+
`local_tarball = "${config.runtime?.local_tarball ?? OPENSHELL_RUNTIME_TARBALL}"`,
|
|
146
|
+
`remote_root = "${config.runtime?.remote_root ?? DEFAULT_RUNTIME_REMOTE_ROOT}"`,
|
|
147
|
+
`synced_at = "${config.runtime?.synced_at ?? new Date().toISOString()}"`,
|
|
148
|
+
"",
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
fs.writeFileSync(OPENSHELL_TOML, lines.join("\n"), { mode: 0o600 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findCliRoot(startDir: string): string {
|
|
154
|
+
let current = startDir;
|
|
155
|
+
while (true) {
|
|
156
|
+
const pkg = path.join(current, "package.json");
|
|
157
|
+
const dist = path.join(current, "dist", "index.js");
|
|
158
|
+
if (fs.existsSync(pkg) && fs.existsSync(dist)) return current;
|
|
159
|
+
const parent = path.dirname(current);
|
|
160
|
+
if (parent === current) throw new Error("Could not locate ARC-402 CLI root from current install path");
|
|
161
|
+
current = parent;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function buildRuntimeTarball(): { cliRoot: string; tarballPath: string } {
|
|
166
|
+
const cliRoot = findCliRoot(__dirname);
|
|
167
|
+
fs.mkdirSync(OPENSHELL_RUNTIME_DIR, { recursive: true, mode: 0o700 });
|
|
168
|
+
|
|
169
|
+
const tar = runCmd(
|
|
170
|
+
"tar",
|
|
171
|
+
[
|
|
172
|
+
"-czf",
|
|
173
|
+
OPENSHELL_RUNTIME_TARBALL,
|
|
174
|
+
"package.json",
|
|
175
|
+
"package-lock.json",
|
|
176
|
+
"dist",
|
|
177
|
+
"node_modules",
|
|
178
|
+
],
|
|
179
|
+
{ timeout: 300000, env: process.env }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!tar.ok) {
|
|
183
|
+
// Retry from the cli root via shell so relative paths resolve correctly.
|
|
184
|
+
const shellTar = runCmd(
|
|
185
|
+
"bash",
|
|
186
|
+
[
|
|
187
|
+
"-lc",
|
|
188
|
+
`cd ${shellEscape(cliRoot)} && tar -czf ${shellEscape(OPENSHELL_RUNTIME_TARBALL)} package.json package-lock.json dist node_modules`,
|
|
189
|
+
],
|
|
190
|
+
{ timeout: 300000 }
|
|
191
|
+
);
|
|
192
|
+
if (!shellTar.ok) {
|
|
193
|
+
throw new Error(shellTar.stderr || shellTar.stdout || "Failed to build ARC-402 runtime tarball");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { cliRoot, tarballPath: OPENSHELL_RUNTIME_TARBALL };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function buildOpenShellSshConfig(sandboxName: string): { configPath: string; host: string } {
|
|
201
|
+
const sshConfig = runCmd("openshell", ["sandbox", "ssh-config", sandboxName], { timeout: 120000 });
|
|
202
|
+
if (!sshConfig.ok || !sshConfig.stdout.trim()) {
|
|
203
|
+
throw new Error(`Failed to get OpenShell SSH config for sandbox ${sandboxName}: ${sshConfig.stderr || sshConfig.stdout || "unknown error"}`);
|
|
204
|
+
}
|
|
205
|
+
const hostMatch = sshConfig.stdout.match(/^Host\s+(\S+)/m);
|
|
206
|
+
if (!hostMatch) {
|
|
207
|
+
throw new Error(`Could not parse OpenShell SSH host alias for sandbox ${sandboxName}`);
|
|
208
|
+
}
|
|
209
|
+
const configPath = path.join(os.tmpdir(), `arc402-openshell-${sandboxName}.ssh`);
|
|
210
|
+
fs.writeFileSync(configPath, sshConfig.stdout, { mode: 0o600 });
|
|
211
|
+
return { configPath, host: hostMatch[1] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function provisionFileToSandbox(sandboxName: string, localPath: string, remotePath: string): void {
|
|
215
|
+
if (!fs.existsSync(localPath)) {
|
|
216
|
+
throw new Error(`Local file not found: ${localPath}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const remoteDir = path.posix.dirname(remotePath);
|
|
220
|
+
const uploadDir = `/tmp/arc402-upload-${Date.now()}`;
|
|
221
|
+
const { configPath, host } = buildOpenShellSshConfig(sandboxName);
|
|
222
|
+
|
|
223
|
+
const prep = runCmd("ssh", ["-F", configPath, host, `rm -rf ${shellEscape(uploadDir)} && mkdir -p ${shellEscape(uploadDir)} && mkdir -p ${shellEscape(remoteDir)}`], { timeout: 120000 });
|
|
224
|
+
if (!prep.ok) {
|
|
225
|
+
throw new Error(`Failed to prepare remote upload directory: ${prep.stderr || prep.stdout}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const upload = runCmd("openshell", ["sandbox", "upload", sandboxName, localPath, uploadDir], { timeout: 300000 });
|
|
229
|
+
if (!upload.ok) {
|
|
230
|
+
throw new Error(`Failed to upload ${path.basename(localPath)}: ${upload.stderr || upload.stdout}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const remoteUploaded = path.posix.join(uploadDir, path.basename(localPath));
|
|
234
|
+
const move = runCmd("ssh", ["-F", configPath, host, `cp ${shellEscape(remoteUploaded)} ${shellEscape(remotePath)}`], { timeout: 120000 });
|
|
235
|
+
if (!move.ok) {
|
|
236
|
+
throw new Error(`Failed to place ${path.basename(localPath)} at ${remotePath}: ${move.stderr || move.stdout}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function provisionRuntimeToSandbox(
|
|
241
|
+
sandboxName: string,
|
|
242
|
+
remoteRoot = DEFAULT_RUNTIME_REMOTE_ROOT,
|
|
243
|
+
): { tarballPath: string; remoteRoot: string } {
|
|
244
|
+
const { tarballPath } = buildRuntimeTarball();
|
|
245
|
+
const remoteUploadDir = `/tmp/arc402-runtime-upload-${Date.now()}`;
|
|
246
|
+
const remoteTarball = `${remoteUploadDir}/arc402-cli-runtime.tgz`;
|
|
247
|
+
const { configPath, host } = buildOpenShellSshConfig(sandboxName);
|
|
248
|
+
|
|
249
|
+
const prep = runCmd("ssh", ["-F", configPath, host, `rm -rf ${shellEscape(remoteUploadDir)} && mkdir -p ${shellEscape(remoteUploadDir)}`], { timeout: 120000 });
|
|
250
|
+
if (!prep.ok) {
|
|
251
|
+
throw new Error(`Failed to prepare remote upload directory: ${prep.stderr || prep.stdout}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const upload = runCmd("openshell", ["sandbox", "upload", sandboxName, tarballPath, remoteUploadDir], { timeout: 300000 });
|
|
255
|
+
if (!upload.ok) {
|
|
256
|
+
throw new Error(`Failed to upload ARC-402 runtime bundle: ${upload.stderr || upload.stdout}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const extract = runCmd(
|
|
260
|
+
"ssh",
|
|
261
|
+
[
|
|
262
|
+
"-F", configPath,
|
|
263
|
+
host,
|
|
264
|
+
`mkdir -p ${shellEscape(remoteRoot)} && tar -xzf ${shellEscape(remoteTarball)} -C ${shellEscape(remoteRoot)} && test -f ${shellEscape(path.posix.join(remoteRoot, "dist/daemon/index.js"))}`,
|
|
265
|
+
],
|
|
266
|
+
{ timeout: 300000 }
|
|
267
|
+
);
|
|
268
|
+
if (!extract.ok) {
|
|
269
|
+
throw new Error(`Failed to provision ARC-402 runtime inside sandbox: ${extract.stderr || extract.stdout}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { tarballPath, remoteRoot };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function shellEscape(value: string): string {
|
|
276
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
277
|
+
}
|