Skip to content
Open
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: 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.
21 changes: 7 additions & 14 deletions contracts/ens/ENSJobPages.sol
Original file line number Diff line number Diff line change
Expand Up @@ -817,10 +817,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 +835,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 +846,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 +1085,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 keeps those write-capability checks as hard configuration gates so hook processing does not falsely report success while metadata/authorisation writes are impossible.
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
8 changes: 4 additions & 4 deletions docs/REFERENCE/ENS_REFERENCE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ENS Reference (Generated)

Generated at (UTC): 1970-01-01T00:00:00Z
Source fingerprint: eb08b0407b10d626
Source fingerprint: b2467e6ec95fafc5

Source files used:
- `contracts/AGIJobManager.sol`
Expand Down Expand Up @@ -55,9 +55,9 @@ Source files used:
- `function lockJobENS(uint256 jobId, address employer, address agent, bool burnFuses) public onlyOwner` ([contracts/ens/ENSJobPages.sol#L767](../../contracts/ens/ENSJobPages.sol#L767))
- `function _lockJobENS(uint256 jobId, address employer, address agent, bool burnFuses) internal` ([contracts/ens/ENSJobPages.sol#L771](../../contracts/ens/ENSJobPages.sol#L771))
- `function _createSubname(bytes32 parentRootNode, string memory label) internal returns (bytes32 node)` ([contracts/ens/ENSJobPages.sol#L798](../../contracts/ens/ENSJobPages.sol#L798))
- `function _isWrappedRootNode(bytes32 rootNode) internal view returns (bool)` ([contracts/ens/ENSJobPages.sol#L887](../../contracts/ens/ENSJobPages.sol#L887))
- `function _requireWrapperAuthorization(bytes32 rootNode) internal view` ([contracts/ens/ENSJobPages.sol#L899](../../contracts/ens/ENSJobPages.sol#L899))
- `function _registerRootVersion(bytes32 rootNode, string memory rootName) internal` ([contracts/ens/ENSJobPages.sol#L1052](../../contracts/ens/ENSJobPages.sol#L1052))
- `function _isWrappedRootNode(bytes32 rootNode) internal view returns (bool)` ([contracts/ens/ENSJobPages.sol#L874](../../contracts/ens/ENSJobPages.sol#L874))
- `function _requireWrapperAuthorization(bytes32 rootNode) internal view` ([contracts/ens/ENSJobPages.sol#L886](../../contracts/ens/ENSJobPages.sol#L886))
- `function _registerRootVersion(bytes32 rootNode, string memory rootName) internal` ([contracts/ens/ENSJobPages.sol#L1039](../../contracts/ens/ENSJobPages.sol#L1039))
- `function verifyENSOwnership(` ([contracts/utils/ENSOwnership.sol#L32](../../contracts/utils/ENSOwnership.sol#L32))
- `function verifyENSOwnership(` ([contracts/utils/ENSOwnership.sol#L48](../../contracts/utils/ENSOwnership.sol#L48))
- `function verifyMerkleOwnership(address claimant, bytes32[] calldata proof, bytes32 merkleRoot)` ([contracts/utils/ENSOwnership.sol#L61](../../contracts/utils/ENSOwnership.sol#L61))
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
43 changes: 43 additions & 0 deletions scripts/ens/lib/ethers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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,
Contract: raw.Contract,
JsonRpcProvider: raw.providers.JsonRpcProvider,
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() };
4 changes: 1 addition & 3 deletions scripts/ens/lib/json_rpc.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#!/usr/bin/env node
const { execFileSync } = require('node:child_process');
const { createRequire } = require('node:module');
const requireFromHere = createRequire(__filename);
const { ethers } = requireFromHere('../../../hardhat/node_modules/ethers');
const { ethers } = require('./ethers');

function toQuantity(value) {
return ethers.toBeHex(value);
Expand Down
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
Loading
Loading