Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ See full behavior details: [`docs/ENS/ENS_JOB_PAGES_OVERVIEW.md`](docs/ENS/ENS_J
Expected result after Prime deployment:
- Premium jobs use procurement-first winner discovery before assignment (not first-touch lock capture).
- Settlement retains conservative escrow/bond/dispute/finalization behavior.
- Optional ENSJobPages lifecycle hooks can be configured on Prime via `setEnsJobPages(...)` and remain best-effort/non-fatal.
- Optional ENSJobPages lifecycle hooks can be configured on Prime via `setEnsJobPages(...)` and remain best-effort/non-fatal. Prime does not expose a `useEnsJobTokenURI` flag; completion NFT metadata remains IPFS/completion-URI based unless a future, separately sized manager release changes that.

Prime exposes keeper/bot-friendly autonomy surfaces for deterministic procurement progression in discovery (`AGIJobDiscoveryPrime`): `isShortlistFinalizable`, `isWinnerFinalizable`, `isFallbackPromotable`, `nextActionForProcurement`, `getAutonomyStatus`, and permissionless `advanceProcurement` for timeout-driven stage advancement.

Expand Down Expand Up @@ -273,7 +273,7 @@ Alias note: `check-no-binaries` is exposed as `npm run check:no-binaries`.

- Preview ENS values are projections from the current prefix/root configuration.
- Effective ENS values are authoritative per-job snapshots stored in `ENSJobPages`.
- `AGIJobManagerPrime` remains on the existing `handleHook(uint8,uint256)` ABI; authority repair and metadata repair live on the ENS side.
- `AGIJobManagerPrime` remains on the existing `handleHook(uint8,uint256)` ABI; authority repair, migration, resolver repair, and finalization live on the ENS side. Preview getters are projections only; authoritative values come from ENSJobPages per-job snapshots.
- Re-run `scripts/ens/audit-mainnet.ts` and `scripts/ens/inventory-job-pages.ts` from a networked operator environment before mainnet cutover because chain state is the source of truth.
- Treat `previewJobEns*` as projected values only; treat `effectiveJobEns*` as authoritative only after the inventory/status scripts confirm authority snapshot readiness for that job.
- The production-safe Prime path is **keeper-assisted authoritative ENS**, not fully automated on-chain ENS hydration: operators may need to call `createJobPage`, `onAgentAssigned`, `onCompletionRequested`, `repairAuthoritySnapshot`, `repairResolver`, `repairTexts`, `repairAuthorisations`, and `lockJobENS` using event-driven runbooks.
25 changes: 8 additions & 17 deletions contracts/ens/ENSJobPages.sol
Original file line number Diff line number Diff line change
Expand Up @@ -440,10 +440,8 @@ contract ENSJobPages is Ownable, ERC1155Holder, IENSJobPagesHooksV1 {
if (!ok || rootOwner == address(0)) failures |= CONFIG_ERR_ROOT_OWNER;
if (ok && rootOwner == address(nameWrapper) && !_isWrapperAuthorizationReady()) failures |= CONFIG_ERR_WRAPPER_APPROVAL;
if (ok && rootOwner != address(0) && rootOwner != address(this) && rootOwner != address(nameWrapper)) failures |= CONFIG_ERR_ROOT_OWNER;
(bool supportsText, bool supportsSetText, bool supportsSetAuthorisation) = _resolverCapabilities();
(bool supportsText,,) = _resolverCapabilities();
if (!supportsText) failures |= CONFIG_ERR_RESOLVER_TEXT;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore write-capability gating in validateConfiguration

Any resolver that exposes a readable text(bytes32,string) surface but does not actually implement setText/setAuthorisation now passes validateConfiguration(). In that setup handleHook() will proceed, create/adopt the subname, and emit ENSHookProcessed(..., configured=true, success=true) because _setTextBestEffort() and _setAuthorisationBestEffort() swallow the missing-function reverts, but the page never gets metadata or permissions and jobEnsReady() stays false. Before this change the ERC-165 write-interface checks prevented that silent misconfiguration.

Useful? React with 👍 / 👎.

if (!supportsSetText) failures |= CONFIG_ERR_RESOLVER_SETTEXT;
if (!supportsSetAuthorisation) failures |= CONFIG_ERR_RESOLVER_SETAUTH;
}

function jobEnsPreview(uint256 jobId) external view returns (string memory) {
Expand Down Expand Up @@ -817,10 +815,6 @@ contract ENSJobPages is Ownable, ERC1155Holder, IENSJobPagesHooksV1 {

function _setResolverBestEffort(uint8 hook, uint256 jobId, bytes32 node, address resolver) internal {
if (resolver == address(0)) return;
if (!_supportsResolverInterface(address(publicResolver), RESOLVER_SETTEXT_INTERFACE_ID)) {
emit ENSHookBestEffortFailure(hook, jobId, "RESOLVER_SET_TEXT_UNSUPPORTED");
}

if (_isWrappedNode(node)) {
try IResolverManager(address(nameWrapper)).setResolver(node, resolver) {
_jobResolverConfigured[jobId] = true;
Expand All @@ -839,10 +833,6 @@ contract ENSJobPages is Ownable, ERC1155Holder, IENSJobPagesHooksV1 {

function _setTextBestEffort(uint8 hook, uint256 jobId, bytes32 node, string memory key, string memory value) internal {
if (bytes(value).length == 0) return;
if (!_supportsResolverInterface(address(publicResolver), RESOLVER_SETTEXT_INTERFACE_ID)) {
emit ENSHookBestEffortFailure(hook, jobId, "SET_TEXT_UNSUPPORTED");
return;
}
try publicResolver.setText(node, key, value) {
if (keccak256(bytes(key)) == keccak256(bytes("agijobs.completion.public"))) {
_jobCompletionTextConfigured[jobId] = true;
Expand All @@ -854,11 +844,6 @@ contract ENSJobPages is Ownable, ERC1155Holder, IENSJobPagesHooksV1 {

function _setAuthorisationBestEffort(uint8 hook, uint256 jobId, bytes32 node, address account, bool authorised) internal {
if (account == address(0)) return;
if (!_supportsResolverInterface(address(publicResolver), RESOLVER_SETAUTH_INTERFACE_ID)) {
emit ENSHookBestEffortFailure(hook, jobId, "SET_AUTH_UNSUPPORTED");
return;
}

try publicResolver.setAuthorisation(node, account, authorised) {
emit JobENSPermissionsUpdated(jobId, account, authorised);
} catch {
Expand Down Expand Up @@ -1098,11 +1083,17 @@ contract ENSJobPages is Ownable, ERC1155Holder, IENSJobPagesHooksV1 {
}

function _resolverCapabilities() internal view returns (bool supportsText, bool supportsSetText, bool supportsSetAuthorisation) {
supportsText = _supportsResolverInterface(address(publicResolver), RESOLVER_TEXT_INTERFACE_ID);
supportsText = _supportsResolverInterface(address(publicResolver), RESOLVER_TEXT_INTERFACE_ID) || _supportsTextLookup(address(publicResolver));
supportsSetText = _supportsResolverInterface(address(publicResolver), RESOLVER_SETTEXT_INTERFACE_ID);
supportsSetAuthorisation = _supportsResolverInterface(address(publicResolver), RESOLVER_SETAUTH_INTERFACE_ID);
}

function _supportsTextLookup(address resolver) internal view returns (bool ok) {
if (resolver == address(0) || resolver.code.length == 0) return false;
bytes memory payload = abi.encodeWithSignature("text(bytes32,string)", bytes32(0), "schema");
(ok, ) = resolver.staticcall(payload);
}

function _supportsResolverInterface(address resolver, bytes4 interfaceId) internal view returns (bool ok) {
if (resolver == address(0) || resolver.code.length == 0) return false;
bytes memory payload = abi.encodeWithSelector(SUPPORTS_INTERFACE_SELECTOR, interfaceId);
Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ It **does not** freeze operational controls like pause, settlement pause, thresh
| `ens` | `updateEnsRegistry` | Yes | identity-configurable + **empty locked balances** + nonzero | Identity gating dependency |
| `nameWrapper` | `updateNameWrapper` | Yes | identity-configurable + **empty locked balances** + nonzero | Wrapped-root checks dependency |
| `ensJobPages` | `setEnsJobPages` | Yes | identity-configurable; contract code required if nonzero | Enables lifecycle hooks |
| `useEnsJobTokenURI` | `setUseEnsJobTokenURI` | Yes | none | Pulls NFT tokenURI from ENSJobPages when available |
| `useEnsJobTokenURI` | Legacy manager only | No on Prime | Prime does not expose this flag | Treat as quarantined/deprecated for Prime docs; completion NFTs stay completion-URI based on current Prime deployments |
| Root nodes | `updateRootNodes` | Yes | identity-configurable + **empty locked balances** | club/agent + alpha variants |
| Merkle roots | `updateMerkleRoots` | Yes | not identity-locked (callable after `lockIdentityConfiguration()`) | validator/agent allowlist roots; owner can update post-lock |
| Moderators | `addModerator/removeModerator` | Yes | nonzero address helper check | Dispute resolution role |
Expand Down
2 changes: 1 addition & 1 deletion docs/ENS/OBSERVED_MAINNET_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ _Observed on Ethereum mainnet on 2026-03-23 UTC using `scripts/ens/audit-mainnet
- `configurationStatus()`
- `jobAuthorityInfo(uint256)`
currently revert on mainnet.
2. The live public resolver configuration is not sufficient for the intended production-grade metadata and authorisation workflow because `setText` and `setAuthorisation` support is not advertised.
2. The live public resolver does not advertise `setText` or `setAuthorisation` support over ERC-165. The replacement ENSJobPages in this repo therefore treats those interface bits as diagnostic signals rather than hard configuration blockers and uses guarded best-effort writes instead.
3. There are currently no Prime jobs on the observed manager deployment (`nextJobId = 0`), so there is no historical inventory to migrate yet on this address pair.

## Compatibility conclusion
Expand Down
14 changes: 5 additions & 9 deletions docs/ens-job-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ This document defines the **official ENS naming scheme** and **record layout** f
One ENS name per job:

```
job-<jobId>.alpha.jobs.agi.eth
agijob-<jobId>.alpha.jobs.agi.eth
```

Example:
```
job-42.alpha.jobs.agi.eth
agijob-42.alpha.jobs.agi.eth
```

`jobId` is the on‑chain AGIJobManager job ID.
Expand Down Expand Up @@ -100,7 +100,7 @@ When using the `ENSJobPages` helper contract, complete these wiring steps:
1. Deploy `ENSJobPages` with the ENS registry, NameWrapper (if any), PublicResolver, root node, and root name.
2. Ensure `alpha.jobs.agi.eth` is owned by the helper (or wrapped and approved for it).
3. Call `ENSJobPages.setJobManager(AGIJobManager)` so hooks are accepted.
4. Call `AGIJobManager.setEnsJobPages(ENSJobPages)` to enable hook callbacks.
4. Call `AGIJobManagerPrime.setEnsJobPages(ENSJobPages)` to enable hook callbacks.

These steps keep ENS integration **opt-in** and ensure lifecycle hooks remain best-effort.

Expand All @@ -112,11 +112,7 @@ These steps keep ENS integration **opt-in** and ensure lifecycle hooks remain be
- Revoke resolver authorizations after terminal settlement.

## ENS job NFT tokenURI (optional)
When `AGIJobManager.setUseEnsJobTokenURI(true)` is enabled (and an ENS helper is configured), completion NFTs point to:
```
ens://job-<jobId>.alpha.jobs.agi.eth
```
When disabled (default), the tokenURI behavior is unchanged and continues to use the completion metadata pointer.
The deployed Prime manager does not expose `setUseEnsJobTokenURI` or `useEnsJobTokenURI`. On the current Prime path, completion NFTs continue to use the completion metadata pointer/IPFS flow. `useEnsJobTokenURI` remains an ENSJobPages compatibility flag only and should be treated as quarantined documentation-wise until a separately sized manager release explicitly wires it end-to-end.

## Post‑terminal lock (optional)
`AGIJobManager.lockJobENS(jobId, burnFuses)` can be called after a terminal state to re‑revoke resolver authorizations and optionally attempt fuse burning (best‑effort).
Expand All @@ -131,6 +127,6 @@ Fuse burning is optional and does **not** affect settlement or withdrawals if it

## 2026-03 authority model

`previewJobEns*` getters now expose mutable projections, while `effectiveJobEns*` getters expose immutable per-job authority snapshots. Compatibility getters (`jobEnsName`, `jobEnsURI`, `jobEnsNode`) prefer authoritative values once established and only fall back to preview values before first issuance/import. Operational repairs are owner-only ENS-side actions: `repairAuthoritySnapshot`, `repairResolver`, `repairTexts`, `repairAuthorisations`, `replayCreate`, `replayAssign`, `replayCompletion`, `replayRevoke`, and `replayLock`. Chain state wins over docs, so mainnet runbooks must be regenerated from `scripts/ens/output/` before cutover.
`previewJobEns*` getters expose mutable projections built from the current prefix/root only. `effectiveJobEns*` getters expose immutable per-job authority snapshots tied to the stored root version and historical label. Compatibility getters (`jobEnsName`, `jobEnsURI`, `jobEnsNode`) prefer authoritative values once established and only fall back to preview values before first issuance/import. Operational repairs are owner-only ENS-side actions: `repairAuthoritySnapshot`, `repairResolver`, `repairTexts`, `repairAuthorisations`, `replayCreate`, `replayAssign`, `replayCompletion`, `replayRevoke`, and `replayLock`. Chain state wins over docs, so mainnet runbooks must be regenerated from `scripts/ens/output/` before cutover.

Production wording matters: the current recommended path is **keeper-assisted authoritative** ENS. Prime settlement stays authoritative and non-blocking even if ENS hydration is delayed or temporarily broken, while operators use `scripts/ens/audit-mainnet.ts`, `scripts/ens/inventory-job-pages.ts`, `scripts/ens/migrate-legacy-batch.ts`, and `scripts/ens/repair-job-page.ts` to classify and repair per-job ENS state.
18 changes: 5 additions & 13 deletions hardhat/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions scripts/ens/audit-mainnet.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const { createRequire } = require('node:module');

const requireFromHere = createRequire(__filename);
const { ethers } = requireFromHere('../../hardhat/node_modules/ethers');
const { ethers } = require('./lib/ethers');
const { CurlJsonRpcProvider } = require('./lib/json_rpc');

const OUTPUT = path.resolve('scripts/ens/output/audit-mainnet.json');
Expand Down
5 changes: 1 addition & 4 deletions scripts/ens/inventory-job-pages.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const { createRequire } = require('node:module');

const requireFromHere = createRequire(__filename);
const { ethers } = requireFromHere('../../hardhat/node_modules/ethers');
const { ethers } = require('./lib/ethers');
const { CurlJsonRpcProvider } = require('./lib/json_rpc');

const OUTPUT = path.resolve('scripts/ens/output/inventory-job-pages.json');
Expand Down
41 changes: 41 additions & 0 deletions scripts/ens/lib/ethers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const { createRequire } = require('node:module');
const path = require('node:path');

const requireFromHere = createRequire(__filename);

function loadEthers() {
const candidates = [
path.resolve(__dirname, '../../../hardhat/node_modules/ethers'),
'ethers',
];
Comment on lines +7 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Finish the portable ethers fallback for RPC-based tools

This new helper does not make the ENS operator scripts portable end-to-end yet. audit-mainnet.ts, inventory-job-pages.ts, migrate-legacy-batch.ts, and repair-job-page.ts now import ./lib/ethers, but they still instantiate CurlJsonRpcProvider, and scripts/ens/lib/json_rpc.js:5 still does a hard require('../../../hardhat/node_modules/ethers'). In the exact fallback scenario this change is meant to support (no hardhat/node_modules/ethers, only a standalone ethers install), those scripts will still fail during module load before any RPC call.

Useful? React with 👍 / 👎.


for (const candidate of candidates) {
try {
const mod = requireFromHere(candidate);
return mod.ethers || mod;
} catch (error) {
if (candidate === candidates[candidates.length - 1]) throw error;
}
}
}

const raw = loadEthers();
const isV6 = typeof raw.ZeroAddress === 'string';

function compat() {
if (isV6) return raw;
const utils = raw.utils;
return {
...raw,
ZeroAddress: raw.constants.AddressZero,
ZeroHash: raw.constants.HashZero,
Interface: utils.Interface,
Wallet: raw.Wallet,
id: utils.id,
namehash: utils.namehash,
ensNormalize: (value) => value.trim().toLowerCase(),
solidityPackedKeccak256: (types, values) => utils.solidityKeccak256(types, values),
Comment on lines +25 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Complete the v5 compatibility shim before using it in phase0

When loadEthers() falls back to an ethers v5 install, compat() only aliases Interface, Wallet, hashing helpers, and constants. scripts/ens/phase0-mainnet-snapshot.mjs now consumes this helper but immediately calls new ethers.JsonRpcProvider(...) and new ethers.Contract(...), which do not exist on the v5-shaped object returned here. In the portability scenario this helper is meant to support (no hardhat/node_modules/ethers, only a standalone ethers install), the phase0 snapshot script will still crash before it can query mainnet.

Useful? React with 👍 / 👎.

Comment on lines +34 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize v5 decoded integers before exposing the compat shim

The v5 branch returns utils.Interface unchanged, so decodeFunctionResult() still yields v5 BigNumber objects, while the scripts you just rewired assume v6-style native bigints. For example, inventory-job-pages.ts does Number(nextJobIdRead), phase0-mainnet-snapshot.mjs calls BigInt(value) in asNumber(), and audit-mainnet.ts only stringifies values whose type is bigint. In the exact fallback scenario this shim targets, those reads will either throw or serialize malformed JSON, so the ENS audit/snapshot tooling still cannot run end-to-end on an ethers v5 install.

Useful? React with 👍 / 👎.

};
Comment on lines +28 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Provide toBeHex in the v5 ethers shim

In the standalone ethers v5 fallback this helper is supposed to support, compat() never defines toBeHex, but scripts/ens/lib/json_rpc.js now unconditionally calls ethers.toBeHex(...) when formatting block tags, nonces, gas, and values. That means audit-mainnet.ts, inventory-job-pages.ts, repair-job-page.ts, and migrate-legacy-batch.ts will still die with TypeError: ethers.toBeHex is not a function as soon as CurlJsonRpcProvider needs any quantity conversion, so the new portability path remains broken for every RPC-backed ENS tool.

Useful? React with 👍 / 👎.

}

module.exports = { ethers: compat() };
5 changes: 1 addition & 4 deletions scripts/ens/migrate-legacy-batch.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const { createRequire } = require('node:module');

const requireFromHere = createRequire(__filename);
const { ethers } = requireFromHere('../../hardhat/node_modules/ethers');
const { ethers } = require('./lib/ethers');
const { CurlJsonRpcProvider } = require('./lib/json_rpc');

const RPC = (process.env.MAINNET_RPC_URL || 'https://ethereum-rpc.publicnode.com').trim();
Expand Down
10 changes: 5 additions & 5 deletions scripts/ens/output/audit-mainnet.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"generatedAt": "2026-03-23T02:55:31.945Z",
"generatedAt": "2026-03-23T04:25:10.398Z",
"network": "ethereum-mainnet",
"rpc": "https://ethereum.publicnode.com",
"rpc": "https://ethereum-rpc.publicnode.com",
"inputs": {
"ensJobPages": "0x97E03F7BFAC116E558A25C8f09aEf09108a2779d",
"agiJobManagerPrime": "0xF8fc6572098DDcAc4560E17cA4A683DF30ea993e",
Expand All @@ -11,9 +11,9 @@
},
"proven": {
"latestBlock": {
"number": "0x1792838",
"hash": "0x1f1fdb2e55121b17bffae167a8f4a18ddbf87a0e59b810852df20c97b115b80f",
"timestamp": "0x69c0ab9b"
"number": "0x17929f5",
"hash": "0x6957bb47b98ac53bc63e079523e16bb4e42589cce22e509da5c6b402caf9115b",
"timestamp": "0x69c0c09b"
},
"expectedRootNode": "0xc164c9558a3c429519a9b2eba9f650025731fccc46b3a5664283bcab84f7e690",
"handleHook": {
Expand Down
8 changes: 4 additions & 4 deletions scripts/ens/output/inventory-job-pages.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"generatedAt": "2026-03-23T02:55:37.263Z",
"rpc": "https://ethereum.publicnode.com",
"generatedAt": "2026-03-23T04:25:16.970Z",
"rpc": "https://ethereum-rpc.publicnode.com",
"prime": "0xF8fc6572098DDcAc4560E17cA4A683DF30ea993e",
"ensJobPages": "0x97E03F7BFAC116E558A25C8f09aEf09108a2779d",
"proven": {
"latestBlock": "24717369",
"latestBlock": "24717814",
"configurationStatus": null
},
"assumed": [],
"jobs": [],
"summary": {
"nextJobId": 0,
"scannedJobs": 0,
"maxJobs": 64,
"maxJobs": 32,
"truncated": false,
"previewOnly": [],
"authoritySnapshotted": [],
Expand Down
4 changes: 2 additions & 2 deletions scripts/ens/output/repair-job-page.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"generatedAt": "2026-03-23T03:47:05.673Z",
"rpc": "https://ethereum.publicnode.com",
"generatedAt": "2026-03-23T04:25:19.296Z",
"rpc": "https://ethereum-rpc.publicnode.com",
"jobId": 0,
"exactLabel": "",
"execute": false,
Expand Down
Loading
Loading